|
1 | | -# SimpleInjection |
| 1 | +# SimpleMVVM |
2 | 2 |
|
3 | | -A lightweight dependency injection library for C# that combines simple DI container functionality with powerful source generation for content management. |
| 3 | +A lightweight WPF MVVM framework with automatic source generation that eliminates boilerplate code while maintaining full control over your view models. |
4 | 4 |
|
5 | 5 | ## Features |
6 | 6 |
|
7 | | -### 🚀 Simple Dependency Injection |
8 | | -- **Attribute-based registration** - Mark classes with `[Singleton]`, `[Scoped]`, or `[Transient]` |
9 | | -- **Automatic service discovery** - No manual registration required |
10 | | -- **Constructor injection** - Automatic dependency resolution |
11 | | -- **Scope management** - Built-in scoped service lifetime management |
12 | | - |
13 | | -### ⚡ Content Source Generation |
14 | | -- **Automatic enum generation** - Creates enums from your content collections |
15 | | -- **Type-safe access** - Generated helper methods for accessing content by enum or index |
16 | | -- **Performance optimized** - Uses `NamedComparer<T>` for fast dictionary lookups |
17 | | -- **Roslyn analyzers** - Enforces best practices and catches common mistakes |
| 7 | +- **Zero Boilerplate Commands**: Transform methods into ICommand properties with a simple `[Command]` attribute |
| 8 | +- **Automatic Property Binding**: Generate observable properties from fields using `[Bind]` attribute |
| 9 | +- **Source Generation**: All code generation happens at compile time with no runtime reflection |
| 10 | +- **Lightweight**: Minimal dependencies and overhead |
| 11 | +- **Type Safe**: Full IntelliSense support and compile-time validation |
| 12 | +- **WPF Optimized**: Built specifically for WPF applications with proper CommandManager integration |
18 | 13 |
|
19 | 14 | ## Quick Start |
20 | 15 |
|
21 | | -### 1. Install the Package |
22 | | -```bash |
23 | | -dotnet add package SimpleInjection |
| 16 | +### Installation |
| 17 | + |
| 18 | +```xml |
| 19 | +<PackageReference Include="SimpleMVVM" Version="0.9.0" /> |
24 | 20 | ``` |
25 | 21 |
|
26 | | -### 2. Dependency Injection Usage |
| 22 | +### Basic Usage |
27 | 23 |
|
28 | | -Mark your classes with lifetime attributes: |
| 24 | +1. **Create a ViewModel**: |
29 | 25 |
|
30 | 26 | ```csharp |
31 | | -[Singleton] |
32 | | -public class DatabaseService |
33 | | -{ |
34 | | - public void Connect() => Console.WriteLine("Connected to database"); |
35 | | -} |
| 27 | +using SimpleMVVM; |
| 28 | +using SimpleMVVM.BaseClasses; |
36 | 29 |
|
37 | | -[Scoped] |
38 | | -public class UserService |
| 30 | +[ViewModel] |
| 31 | +public partial class MainViewModel : BaseViewModel |
39 | 32 | { |
40 | | - private readonly DatabaseService _database; |
41 | | - |
42 | | - public UserService(DatabaseService database) |
| 33 | + [Command] |
| 34 | + public void SaveData() |
43 | 35 | { |
44 | | - _database = database; |
| 36 | + // Your save logic here |
| 37 | + MessageBox.Show("Data saved!"); |
| 38 | + } |
| 39 | + |
| 40 | + [Command] |
| 41 | + public void LoadData() |
| 42 | + { |
| 43 | + // Your load logic here |
| 44 | + MessageBox.Show("Data loaded!"); |
45 | 45 | } |
46 | | - |
47 | | - public void GetUser() => _database.Connect(); |
48 | 46 | } |
49 | 47 | ``` |
50 | 48 |
|
51 | | -Initialize and use the host: |
| 49 | +2. **Bind to XAML**: |
| 50 | + |
| 51 | +```xml |
| 52 | +<Window x:Class="MyApp.MainWindow" |
| 53 | + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| 54 | + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
| 55 | + <StackPanel> |
| 56 | + <Button Content="Save" Command="{Binding SaveDataCommand}" /> |
| 57 | + <Button Content="Load" Command="{Binding LoadDataCommand}" /> |
| 58 | + </StackPanel> |
| 59 | +</Window> |
| 60 | +``` |
| 61 | + |
| 62 | +3. **Set DataContext**: |
52 | 63 |
|
53 | 64 | ```csharp |
54 | | -var host = Host.Initialize(); |
| 65 | +public partial class MainWindow : Window |
| 66 | +{ |
| 67 | + public MainWindow() |
| 68 | + { |
| 69 | + InitializeComponent(); |
| 70 | + DataContext = new MainViewModel(); |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
55 | 74 |
|
56 | | -// Get singleton services directly |
57 | | -var dbService = host.Get<DatabaseService>(); |
| 75 | +That's it! The source generator automatically creates `SaveDataCommand` and `LoadDataCommand` properties for you. |
58 | 76 |
|
59 | | -// Create scopes for scoped services |
60 | | -using var scope = host.CreateScope(); |
61 | | -var userService = scope.Get<UserService>(); |
62 | | -``` |
| 77 | +## Advanced Features |
63 | 78 |
|
64 | | -### 3. Content Generation Usage |
| 79 | +### Observable Properties |
65 | 80 |
|
66 | | -Define your content classes: |
| 81 | +Use the `[Bind]` attribute to automatically generate observable properties: |
67 | 82 |
|
68 | 83 | ```csharp |
69 | | -// Your content item must implement INamed |
70 | | -public record Material(string Name, string Color, int Durability) : INamed; |
71 | | - |
72 | | -// Your content collection must implement IContent<T> |
73 | | -[Singleton] |
74 | | -public partial class Materials : IContent<Material> |
| 84 | +[ViewModel] |
| 85 | +public partial class UserViewModel : BaseViewModel |
75 | 86 | { |
76 | | - public Material[] All { get; } = |
77 | | - [ |
78 | | - new("Steel", "Gray", 100), |
79 | | - new("Wood", "Brown", 50), |
80 | | - new("Gold", "Yellow", 25) |
81 | | - ]; |
| 87 | + [Bind] |
| 88 | + private string _firstName = ""; |
| 89 | + |
| 90 | + [Bind] |
| 91 | + private string _lastName = ""; |
| 92 | + |
| 93 | + [Bind] |
| 94 | + private int _age; |
82 | 95 | } |
83 | 96 | ``` |
84 | 97 |
|
85 | | -The source generator automatically creates: |
| 98 | +Generated code includes proper `INotifyPropertyChanged` implementation: |
86 | 99 |
|
87 | 100 | ```csharp |
88 | | -// Generated enum |
89 | | -public enum MaterialsType |
| 101 | +public string FirstName |
90 | 102 | { |
91 | | - Steel, |
92 | | - Wood, |
93 | | - Gold |
| 103 | + get => _firstName; |
| 104 | + set => SetProperty(ref _firstName, value); |
94 | 105 | } |
| 106 | +``` |
95 | 107 |
|
96 | | -// Generated helper methods |
97 | | -public partial class Materials |
| 108 | +### Command with Parameters |
| 109 | + |
| 110 | +Commands automatically support parameters: |
| 111 | + |
| 112 | +```csharp |
| 113 | +[ViewModel] |
| 114 | +public partial class DocumentViewModel : BaseViewModel |
98 | 115 | { |
99 | | - public Material Get(MaterialsType type) => All[(int)type]; |
100 | | - public Material this[MaterialsType type] => All[(int)type]; |
101 | | - public Material GetById(int id) => All[id]; |
102 | | - public Material Steel => All[0]; |
103 | | - public Material Wood => All[1]; |
104 | | - public Material Gold => All[2]; |
| 116 | + [Command] |
| 117 | + public void DeleteItem(object parameter) |
| 118 | + { |
| 119 | + if (parameter is string itemId) |
| 120 | + { |
| 121 | + // Delete logic here |
| 122 | + } |
| 123 | + } |
105 | 124 | } |
106 | 125 | ``` |
107 | 126 |
|
108 | | -Use the generated code: |
| 127 | +### Integration with Dependency Injection |
109 | 128 |
|
110 | | -```csharp |
111 | | -var materials = host.Get<Materials>(); |
| 129 | +SimpleMVVM works seamlessly with dependency injection containers: |
112 | 130 |
|
113 | | -// Type-safe access using enums |
114 | | -var steel = materials[MaterialsType.Steel]; |
115 | | -var wood = materials.Get(MaterialsType.Wood); |
| 131 | +```csharp |
| 132 | +// Using SimpleInjection (companion package) |
| 133 | +[Singleton][ViewModel] |
| 134 | +public partial class MainViewModel : BaseViewModel |
| 135 | +{ |
| 136 | + private readonly IDataService _dataService; |
| 137 | + |
| 138 | + public MainViewModel(IDataService dataService) |
| 139 | + { |
| 140 | + _dataService = dataService; |
| 141 | + } |
| 142 | + |
| 143 | + [Command] |
| 144 | + public async void LoadData() |
| 145 | + { |
| 146 | + var data = await _dataService.GetDataAsync(); |
| 147 | + // Handle data |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
116 | 151 |
|
117 | | -// Direct property access |
118 | | -var gold = materials.Gold; |
| 152 | +## How It Works |
119 | 153 |
|
120 | | -// Index-based access |
121 | | -var firstMaterial = materials.GetById(0); |
122 | | -``` |
| 154 | +SimpleMVVM uses Roslyn source generators to analyze your code at compile time and automatically generate: |
123 | 155 |
|
124 | | -## Advanced Features |
| 156 | +1. **Command Classes**: Each `[Command]` method gets a corresponding `ICommand` implementation |
| 157 | +2. **Command Properties**: Properties that expose the commands for data binding |
| 158 | +3. **Observable Properties**: Properties with `INotifyPropertyChanged` support for `[Bind]` fields |
125 | 159 |
|
126 | | -### Performance Optimizations |
| 160 | +All generated code is available in IntelliSense and can be debugged normally. |
127 | 161 |
|
128 | | -The library includes `NamedComparer<T>` for efficient dictionary operations with `INamed` keys: |
| 162 | +## Generated Code Example |
129 | 163 |
|
| 164 | +**Your Code:** |
130 | 165 | ```csharp |
131 | | -// Use ToNamedDictionary extension method |
132 | | -var materialDict = materials.All.ToNamedDictionary(m => m.Durability); |
133 | | - |
134 | | -// Or explicitly specify the comparer |
135 | | -var dict = new Dictionary<Material, int>(new NamedComparer<Material>()); |
| 166 | +[ViewModel] |
| 167 | +public partial class MyViewModel : BaseViewModel |
| 168 | +{ |
| 169 | + [Command] |
| 170 | + public void DoSomething() => Console.WriteLine("Done!"); |
| 171 | +} |
136 | 172 | ``` |
137 | 173 |
|
138 | | -### SubContent Collections |
139 | | - |
140 | | -For hierarchical content organization: |
141 | | - |
| 174 | +**Generated Code:** |
142 | 175 | ```csharp |
143 | | -public class WeaponStats : ISubContent<Material, int> |
| 176 | +public partial class MyViewModel |
144 | 177 | { |
145 | | - public Dictionary<Material, int> ByKey { get; } |
146 | | - public int this[Material material] => ByKey[material]; |
| 178 | + private DoSomethingCommand? _doSomethingCommand; |
| 179 | + public DoSomethingCommand DoSomethingCommand => _doSomethingCommand ??= new DoSomethingCommand(this); |
| 180 | +} |
| 181 | + |
| 182 | +public sealed class DoSomethingCommand : BaseCommand |
| 183 | +{ |
| 184 | + private readonly MyViewModel _viewModel; |
| 185 | + |
| 186 | + public DoSomethingCommand(MyViewModel viewModel) |
| 187 | + { |
| 188 | + _viewModel = viewModel; |
| 189 | + } |
| 190 | + |
| 191 | + public override void Execute(object? parameter) |
| 192 | + { |
| 193 | + _viewModel.DoSomething(); |
| 194 | + } |
147 | 195 | } |
148 | 196 | ``` |
149 | 197 |
|
150 | | -### Roslyn Analyzers |
| 198 | +## Best Practices |
| 199 | + |
| 200 | +1. **Inherit from BaseViewModel**: Always inherit from `BaseViewModel` for proper `INotifyPropertyChanged` support |
| 201 | +2. **Use Partial Classes**: Mark your view models as `partial` to allow source generation |
| 202 | +3. **Async Commands**: For async operations, use `async void` in command methods |
| 203 | +4. **Parameter Validation**: Always validate parameters in command methods |
| 204 | +5. **Dependency Injection**: Use constructor injection for services and dependencies |
| 205 | + |
| 206 | +## Troubleshooting |
| 207 | + |
| 208 | +### Generator Not Running |
151 | 209 |
|
152 | | -The package includes analyzers that help you: |
153 | | -- **NC001**: Ensures `Dictionary<TKey, TValue>` uses `NamedComparer<T>` for `INamed` keys |
154 | | -- **TND001**: Suggests using `ToNamedDictionary()` instead of `ToDictionary()` for `INamed` keys |
| 210 | +If the source generator isn't creating commands: |
| 211 | + |
| 212 | +1. Ensure you have the `[ViewModel]` attribute on your class |
| 213 | +2. Make sure the class is marked as `partial` |
| 214 | +3. Verify you're inheriting from `BaseViewModel` |
| 215 | +4. Check that methods have the `[Command]` attribute |
| 216 | +5. Clean and rebuild your solution |
| 217 | + |
| 218 | +### Missing Commands in XAML |
| 219 | + |
| 220 | +If commands aren't appearing in XAML IntelliSense: |
| 221 | + |
| 222 | +1. Rebuild the project to trigger source generation |
| 223 | +2. Check that the generated files are created (enable `<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>` to see them) |
| 224 | +3. Ensure proper namespace imports in XAML |
155 | 225 |
|
156 | 226 | ## Requirements |
157 | 227 |
|
158 | 228 | - .NET 8.0 or .NET 9.0 |
159 | | -- C# with nullable reference types enabled (recommended) |
| 229 | +- Windows (WPF applications only) |
| 230 | +- C# 10.0 or later |
160 | 231 |
|
161 | | -## How It Works |
| 232 | +## License |
162 | 233 |
|
163 | | -1. **Service Discovery**: The host scans all loaded assemblies for classes marked with lifetime attributes |
164 | | -2. **Dependency Resolution**: Constructor parameters are automatically resolved from registered services |
165 | | -3. **Source Generation**: The generator scans for classes implementing `IContent<T>` and generates enums and helper methods |
166 | | -4. **Code Analysis**: Roslyn analyzers ensure best practices for dictionary usage with named keys |
| 234 | +MIT License - see LICENSE file for details. |
167 | 235 |
|
168 | | -## Best Practices |
| 236 | +## Contributing |
169 | 237 |
|
170 | | -- Use `[Singleton]` for stateless services and shared resources |
171 | | -- Use `[Scoped]` for services that should be unique per operation/request |
172 | | -- Use `[Transient]` for lightweight, stateless services that need fresh instances |
173 | | -- Always implement `INamed` for content objects to enable source generation |
174 | | -- Use the generated enums for type-safe content access |
175 | | -- Leverage `ToNamedDictionary()` for performance when working with `INamed` collections |
| 238 | +Contributions are welcome! Please feel free to submit issues and pull requests. |
176 | 239 |
|
177 | | -## License |
| 240 | +## Related Packages |
178 | 241 |
|
179 | | -MIT License - see the license file for details. |
| 242 | +- **SimpleInjection**: Companion dependency injection container with automatic service discovery |
0 commit comments