O
O
Oleg Shakhmatov2020-11-16 13:23:00
.NET
Oleg Shakhmatov, 2020-11-16 13:23:00

How to correctly implement ViewModel's information on MVVM?

There is a Search class, which is responsible for searching for files according to the specified criteria, and there is a ViewModel that stores an instance of this class. I need the VM to know how many files were checked, how many matches, and which folder is currently being searched. How should I implement this so that binding can be done? At the moment I have a separate model class that inherits from my ViewModelBase. I suspect that this is far from the best solution, especially considering that I am passing an instance of this class (SearchInfo) to the constructor of the Search model class.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
B
Boris the Animal, 2020-11-17
@Deflarten

Read the comments in the code, including why lock is there. Suddenly you will use such a search with cancellation not in the UI thread (not this particular code, but the logic itself).
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;
        }
    }
}

I started it up quickly like that. You don't really need this:
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 with dependencies
<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>

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question