MVVM
MVVM (Model-View-ViewModel) е архитектурен шаблон за организиране на кода в приложения. Той разделя приложението на три основни компонента:
- Model – съдържа бизнес логиката и данните
- View – представя потребителския интерфейс
- ViewModel – свързва Model и View, съдържа презентационната логика
Базов клас за ViewModel
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Пример за Model
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string FullName => $"{FirstName} {LastName}";
public int Age => DateTime.Now.Year - DateOfBirth.Year;
}
Пример за ViewModel
public class PersonViewModel : ViewModelBase
{
private readonly Person _person;
private readonly IPersonService _personService;
private string _firstName;
public string FirstName
{
get => _firstName;
set
{
if (SetProperty(ref _firstName, value))
{
SaveCommand.RaiseCanExecuteChanged();
}
}
}
private string _lastName;
public string LastName
{
get => _lastName;
set
{
if (SetProperty(ref _lastName, value))
{
SaveCommand.RaiseCanExecuteChanged();
}
}
}
public ICommand SaveCommand { get; }
public ICommand DeleteCommand { get; }
public PersonViewModel(Person person, IPersonService personService)
{
_person = person;
_personService = personService;
FirstName = person.FirstName;
LastName = person.LastName;
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
DeleteCommand = new RelayCommand(ExecuteDelete);
}
private bool CanSave()
{
return !string.IsNullOrEmpty(FirstName) &&
!string.IsNullOrEmpty(LastName);
}
private async Task ExecuteSave()
{
_person.FirstName = FirstName;
_person.LastName = LastName;
await _personService.SavePersonAsync(_person);
}
private async Task ExecuteDelete()
{
await _personService.DeletePersonAsync(_person.Id);
}
}
Пример за View
<Window x:Class="MyApp.Views.PersonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Person Details">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Label Content="First Name:"/>
<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Grid.Row="1">
<Label Content="Last Name:"/>
<TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="Save"
Command="{Binding SaveCommand}"
Width="80"
Margin="0,0,10,0"/>
<Button Content="Delete"
Command="{Binding DeleteCommand}"
Width="80"/>
</StackPanel>
</Grid>
</Window>
Команди (Commands)
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke() ?? true;
}
public void Execute(object parameter)
{
_execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke((T)parameter) ?? true;
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
Dependency Injection
public class App : Application
{
private readonly IServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
// Регистриране на сервиси
services.AddSingleton<IPersonService, PersonService>();
services.AddTransient<PersonViewModel>();
services.AddTransient<PersonView>();
}
protected override void OnStartup(StartupEventArgs e)
{
var mainWindow = _serviceProvider.GetService<PersonView>();
mainWindow.Show();
}
}
Добри практики
- Разделяне на отговорностите
// View - само UI логика
public partial class PersonView : Window
{
public PersonView(PersonViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
// ViewModel - презентационна логика
public class PersonViewModel : ViewModelBase
{
// Логика за представяне на данните
}
// Model - бизнес логика
public class Person
{
// Бизнес логика и данни
}
- Използване на интерфейси за сервиси
public interface IPersonService
{
Task<Person> GetPersonAsync(int id);
Task SavePersonAsync(Person person);
Task DeletePersonAsync(int id);
}
- Async/Await в командите
public class AsyncRelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public async Task ExecuteAsync()
{
try
{
_isExecuting = true;
RaiseCanExecuteChanged();
await _execute();
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
}
- Валидация
public class PersonViewModel : ViewModelBase, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new();
public bool HasErrors => _errors.Any();
private void ValidateFirstName()
{
var errors = new List<string>();
if (string.IsNullOrEmpty(FirstName))
{
errors.Add("First name is required");
}
SetErrors(nameof(FirstName), errors);
}
private void SetErrors(string propertyName, List<string> errors)
{
if (errors.Any())
{
_errors[propertyName] = errors;
}
else
{
_errors.Remove(propertyName);
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
Graph representation of the MVVM pattern
![]()
Model
The Model represents the data and business logic of the application.