• Как правильно реализовать информирование ViewModel'и по MVVM?

    Casper-SC
    @Casper-SC
    Программист (.NET)
    Читай комментарии в коде, в том числе зачем там lock. Вдруг ты будешь использовать такой поиск с отменой не в UI потоке (не именно этот код, а саму логику).

    Models/SearchAlgorithm.cs
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace EventsInModel.Models
    {
        public class SearchAlgorithm
        {
            public string CurrentFolder { get; private set; }
    
            public event EventHandler ProgressChanged;
    
            public async Task Search(CancellationToken cancellationToken)
            {
                for (int i = 0; i < 5; i++)
                {
                    await Task.Delay(1200, cancellationToken);
    
                    cancellationToken.ThrowIfCancellationRequested();
    
                    // Можно прогресс передавать и в качестве аргумента события,
                    // но в данном случае, вряд ли это оправдано. Обработав событие можно получить
                    // доступ к отправителю события и прочитать его свойства.
                    CurrentFolder = i.ToString();
                    ProgressChanged?.Invoke(this, EventArgs.Empty);
                }
            }
        }
    }


    ViewModels/MainViewModel.cs
    using System;
    using System.Threading;
    using System.Windows.Input;
    using EventsInModel.Models;
    using GalaSoft.MvvmLight;
    using GalaSoft.MvvmLight.Command;
    
    namespace EventsInModel.ViewModels
    {
        // ViewModelBase из библиотеки MvvmLight
        public class MainViewModel : ViewModelBase
        {
            //private readonly object _sync = new object();
            private readonly SearchAlgorithm _search;
    
            private string _currentFolder;
            // Логику с отменой можно вынести в отдельный класс, чтобы не писать простыню
            // с отменой в каждом таком месте с операцией, которая может быть отменена, а 
            // в UI приложениях такое сплошь и рядом.
            private volatile CancellationTokenSource _lastCancellationTokenSource;
    
            public string CurrentFolder
            {
                get { return _currentFolder; }
                private set { Set(ref _currentFolder, value); }
            }
    
            public ICommand SearchCommand { get; }
    
            public MainViewModel(SearchAlgorithm search)
            {
                _search = search;
                _search.ProgressChanged += OnSearchProgressChanged;
                SearchCommand = new RelayCommand(Search);
            }
    
            public override void Cleanup()
            {
                //lock (_sync)
                {
                    _lastCancellationTokenSource?.Cancel();
                }
    
                _search.ProgressChanged -= OnSearchProgressChanged;
                base.Cleanup();
            }
    
            /// <summary>
            /// Прерывает прошлый поиск и запускает новый.
            /// </summary>
            private async void Search()
            {
                CancellationTokenSource currentTokenSource;
                // В случае, если такой метод вызывать не из UI потока, то lock здесь нужен
                // Если использовать только из UI потока как здесь, то lock можно удалить.
                // Ещё бы я вынес логику в отдельный класс и использовал в других проектах в том числе.
                //lock (_sync)
                {
                    _lastCancellationTokenSource?.Cancel();
                    currentTokenSource = new CancellationTokenSource();
                    _lastCancellationTokenSource = currentTokenSource;
                }
    
                try
                {
                    await _search.Search(currentTokenSource.Token);
                }
                catch (OperationCanceledException)
                {
                    // Ignored.
                }
                finally
                {
                    //lock (_sync)
                    {
                        currentTokenSource.Dispose();
                        if (ReferenceEquals(_lastCancellationTokenSource, currentTokenSource))
                        {
                            _lastCancellationTokenSource = null;
                        }
                    }
                }
            }
    
            private void OnSearchProgressChanged(object sender, EventArgs e)
            {
                var search = (SearchAlgorithm)sender;
                CurrentFolder = search.CurrentFolder;
            }
        }
    }


    Затестил я по быстрому так. Это тебе не особо нужно:

    MainWindow.xaml.cs
    using System.Windows;
    using EventsInModel.Models;
    using EventsInModel.ViewModels;
    
    namespace EventsInModel
    {
        public partial class MainWindow : Window
        {
            private readonly MainViewModel _viewModel;
    
            public MainWindow()
            {
                InitializeComponent();
                _viewModel = new MainViewModel(new SearchAlgorithm());
                DataContext = _viewModel;
    
                Loaded += OnLoaded;
                Closing += OnClosing;
            }
    
            private void OnLoaded(object sender, RoutedEventArgs e)
            {
                _viewModel.SearchCommand.Execute(null);
                _viewModel.SearchCommand.Execute(null);
            }
    
            private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
            {
                _viewModel.Cleanup();
            }
        }
    }


    Проект с зависимостями
    <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>netcoreapp3.1</TargetFramework>
        <UseWPF>true</UseWPF>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="MvvmLightLibsStd10" Version="5.4.1.1" />
      </ItemGroup>
    
    </Project>
    Ответ написан
    1 комментарий