https://github.com/olivegamestudio/olivestudio.toolkit
A C# MVVM toolkit library that extends CommunityToolkit.Mvvm with additional strongly-typed abstractions for building robust MVVM applications.
https://github.com/olivegamestudio/olivestudio.toolkit
command csharp helpers icommand library mvvm observableobject utility
Last synced: 3 months ago
JSON representation
A C# MVVM toolkit library that extends CommunityToolkit.Mvvm with additional strongly-typed abstractions for building robust MVVM applications.
- Host: GitHub
- URL: https://github.com/olivegamestudio/olivestudio.toolkit
- Owner: olivegamestudio
- Created: 2024-07-22T20:37:42.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-07-24T13:16:27.000Z (3 months ago)
- Last Synced: 2025-07-24T17:54:53.365Z (3 months ago)
- Topics: command, csharp, helpers, icommand, library, mvvm, observableobject, utility
- Language: C#
- Homepage:
- Size: 21.5 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# OliveStudio.Toolkit
A C# MVVM toolkit library that extends CommunityToolkit.Mvvm with additional strongly-typed abstractions for building robust MVVM applications.
## Installation
```bash
# Package manager
Install-Package OliveStudio.Toolkit# .NET CLI
dotnet add package OliveStudio.Toolkit
```## Dependencies
This library extends:
- **CommunityToolkit.Mvvm** - For base MVVM functionality
- **OliveStudio.Helpers** - For async event handler delegates## Core Components
### `ObservableObject`
A generic base class that combines CommunityToolkit.Mvvm's `ObservableObject` with a strongly-typed model, providing a clean separation between your view models and domain models.
```csharp
public abstract class ObservableObject : ObservableObject
{
public TModel Model { get; }
protected ObservableObject(TModel model) { }
protected ObservableObject() { }
}
```**Benefits:**
- Strongly-typed access to your domain model
- Inherits all CommunityToolkit.Mvvm observable functionality
- Clear separation of concerns between UI and business logic### `ICommand`
A strongly-typed command interface that accepts a specific parameter type, providing better type safety than the standard `ICommand`.
```csharp
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(T parameter);
void Execute(T parameter);
}
```### `ICommandAsync`
An asynchronous command interface for handling async operations with strongly-typed parameters.
```csharp
public interface ICommandAsync
{
event AsyncEventHandler CanExecuteChanged;
bool CanExecute(T parameter);
void Execute(T parameter);
}
```**Note:** Uses `AsyncEventHandler` from OliveStudio.Helpers for async event handling.
## Usage Examples
### Observable Object with Model
```csharp
// Domain model
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}// View model
public class UserViewModel : ObservableObject
{
public UserViewModel(User user) : base(user)
{
}
// Expose model properties with change notification
public string Name
{
get => Model.Name;
set => SetProperty(Model.Name, value, Model, (model, val) => model.Name = val);
}
public string Email
{
get => Model.Email;
set => SetProperty(Model.Email, value, Model, (model, val) => model.Email = val);
}
// Computed properties
public string DisplayName => $"{Model.Name} ({Model.Email})";
// Can access the underlying model directly
public DateTime CreatedAt => Model.CreatedAt;
}
```### Command Implementation Example
While the library provides the interfaces, here's how you might implement them:
```csharp
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func _canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action execute, Func canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(T parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(T parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}// Usage in view model
public class ProductViewModel : ObservableObject
{
public ICommand SaveCommand { get; }
public ICommand DeleteCommand { get; }
public ProductViewModel(Product product) : base(product)
{
SaveCommand = new RelayCommand(SaveProduct, CanSaveProduct);
DeleteCommand = new RelayCommand(DeleteProduct);
}
private bool CanSaveProduct(Product product) => !string.IsNullOrEmpty(product?.Name);
private void SaveProduct(Product product) { /* Save logic */ }
private void DeleteProduct(int productId) { /* Delete logic */ }
}
```### Async Command Implementation Example
```csharp
public class AsyncRelayCommand : ICommandAsync
{
private readonly Func _executeAsync;
private readonly Func _canExecute;
public event AsyncEventHandler CanExecuteChanged;
public AsyncRelayCommand(Func executeAsync, Func canExecute = null)
{
_executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
_canExecute = canExecute;
}
public bool CanExecute(T parameter) => _canExecute?.Invoke(parameter) ?? true;
public async void Execute(T parameter) => await _executeAsync(parameter);
public async Task RaiseCanExecuteChangedAsync()
{
if (CanExecuteChanged != null)
await CanExecuteChanged(this, EventArgs.Empty);
}
}// Usage in view model
public class DataViewModel : ObservableObject
{
public ICommandAsync LoadDataCommand { get; }
public DataViewModel(DataModel model) : base(model)
{
LoadDataCommand = new AsyncRelayCommand(LoadDataAsync);
}
private async Task LoadDataAsync(string filter)
{
// Async data loading logic
var data = await _dataService.LoadAsync(filter);
Model.Items = data;
OnPropertyChanged(nameof(Model));
}
}
```## Advanced Patterns
### Collection View Models
```csharp
public class UserListViewModel : ObservableObject>
{
public ObservableCollection Users { get; }
public ICommand SelectUserCommand { get; }
public ICommandAsync SearchCommand { get; }
public UserListViewModel(IList users) : base(users)
{
Users = new ObservableCollection(
users.Select(u => new UserViewModel(u)));
SelectUserCommand = new RelayCommand(SelectUser);
SearchCommand = new AsyncRelayCommand(SearchUsersAsync);
}
private void SelectUser(User user)
{
SelectedUser = Users.FirstOrDefault(vm => vm.Model == user);
}
private async Task SearchUsersAsync(string searchTerm)
{
var filteredUsers = await _userService.SearchAsync(searchTerm);
Model.Clear();
foreach (var user in filteredUsers)
{
Model.Add(user);
Users.Add(new UserViewModel(user));
}
}
[ObservableProperty]
private UserViewModel _selectedUser;
}
```### Nested Models
```csharp
public class OrderViewModel : ObservableObject
{
public CustomerViewModel Customer { get; }
public ObservableCollection Items { get; }
public OrderViewModel(Order order) : base(order)
{
Customer = new CustomerViewModel(order.Customer);
Items = new ObservableCollection(
order.Items.Select(item => new OrderItemViewModel(item)));
}
public decimal TotalAmount => Items.Sum(item => item.Total);
// Forward property changes from nested view models
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == nameof(Items))
{
OnPropertyChanged(nameof(TotalAmount));
}
}
}
```## Integration with CommunityToolkit.Mvvm
This library works seamlessly with CommunityToolkit.Mvvm features:
```csharp
public partial class ProductViewModel : ObservableObject
{
public ProductViewModel(Product product) : base(product)
{
}
// Use CommunityToolkit.Mvvm source generators
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _statusMessage;
// Relay commands from CommunityToolkit.Mvvm
[RelayCommand]
private async Task SaveAsync()
{
IsLoading = true;
try
{
await _productService.SaveAsync(Model);
StatusMessage = "Product saved successfully";
}
finally
{
IsLoading = false;
}
}
// Strongly-typed commands from this library
public ICommand ValidateCommand { get; }
}
```## Best Practices
### 1. Keep Models Pure
```csharp
// ❌ Don't put UI logic in models
public class User
{
public string Name { get; set; }
public bool IsVisible { get; set; } // UI concern
}// ✅ Keep models focused on business logic
public class User
{
public string Name { get; set; }
public UserRole Role { get; set; }
}public class UserViewModel : ObservableObject
{
public bool IsVisible => Model.Role != UserRole.Hidden;
}
```### 2. Use Strongly-Typed Commands
```csharp
// ❌ Weak typing requires casting
public ICommand DeleteCommand { get; } // object parameter// ✅ Strong typing prevents runtime errors
public ICommand DeleteCommand { get; } // int parameter
```### 3. Expose Model Properties Appropriately
```csharp
public class ProductViewModel : ObservableObject
{
// ✅ Expose with change notification for bindable properties
public string Name
{
get => Model.Name;
set => SetProperty(Model.Name, value, Model, (m, v) => m.Name = v);
}
// ✅ Direct access for read-only properties
public DateTime CreatedAt => Model.CreatedAt;
// ✅ Computed properties based on model state
public bool IsNew => Model.Id == 0;
}
```