Прескочи до съдържанието
Редактирай

MVVM

MVVM

MVVM (Model-View-ViewModel) е архитектурен шаблон за организиране на кода в приложения. Той разделя приложението на три основни компонента:

Базов клас за 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();
    }
}

Добри практики

  1. Разделяне на отговорностите
// View - само UI логика
public partial class PersonView : Window
{
    public PersonView(PersonViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
    }
}

// ViewModel - презентационна логика
public class PersonViewModel : ViewModelBase
{
    // Логика за представяне на данните
}

// Model - бизнес логика
public class Person
{
    // Бизнес логика и данни
}
  1. Използване на интерфейси за сервиси
public interface IPersonService
{
    Task<Person> GetPersonAsync(int id);
    Task SavePersonAsync(Person person);
    Task DeletePersonAsync(int id);
}
  1. 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();
        }
    }
}
  1. Валидация
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

MVVM pattern

Model

The Model represents the data and business logic of the application.