Как лучше подключаться к команде со стороны XAML?

Обычно я делаю привязку вот так вот - Command="{Binding SomeCommand}", где SomeCommand это команда из DataContext.

Но вот в этом учебном проекте https://github.com/xellans/LearnMvvm.git я столкнулся с необычным для меня вариантом

<RadioButton Command="{local:SetCurrentContext}" CommandParameter="PersonVM" Content="{DynamicResource Lang1}"></RadioButton>

Приемлемый ли это способ привязки к командам? И какой способ считается более правильным?


Ответы (1 шт):

Автор решения: EvgeniyZ

Для того, чтобы данная привязка работала, ваша команда (и любой другой объект для привязки) должна быть статичная, открываем код, смотрим, и действительно:

public static readonly RoutedUICommand SetCurrentContext = new RoutedUICommand(
    "Задание текущего контекста навигатору, находящемуся в ресурсах, вызвашего команду, элемента.",
    "SetCurrentContext",
    typeof(NavigatorLocator));

Ну а статика зло, ибо как минимум

  • Статика плохо тестируется
  • Статика вечно висит в памяти даже тогда, когда нам этот объект вовсе не нужен
  • Ну и др., тонкости, которые надо учитывать

Я не говорю, что у вас не должно быть статики, я говорю, что лучше обходиться без нее, если есть такая возможность, а в этом конкретном случае, такая возможность есть.

А теперь подумайте, как вы будете реализовать другие свои команды для привязки вида {local:SetCurrentContext}? Плодить тонну статичных объектов? Думаю и сами понимаете, что это глупое решение.

Так что, для единичного использования, как в примере, локатора (о нем поговорим чуть позже), такой подход может и имеет смысл, но делать все свойства для привязки статичными, это стрелять себе в ногу. Если более детальней изучить код из примера, то увидите там дальше нормальные команды и нормальные привязки, например, AuthUserControl.xaml, в нем есть Command="{Binding AuthorizeCommand}", а в VM уже стандартное, не статичное свойство public RelayCommand AuthorizeCommand { get; }.

Теперь что касается локатора
Скажу честно, мне данный подход не нравиться по нескольким причинам:

  1. Статика - как и писал выше, я против нее, а локаторы зачастую на ней основаны.
  2. Локаторы часто внедряются в ресурсы (View слой), и именно XAML им руководит (создает, хранит, управляет сроком жизни, и т.д.), а это значит, что мы теряем полный контроль над ним, что очень плохо. Также подобное плодит такие костыли, как в показанном примере (файл App.xaml.cs -> (NavigatorLocator)this.FindResource("locator");).
  3. Код сильно усложняется ради того, что можно сделать куда проще.

Локатор хорош тогда, когда у вас очень большой проект, и то не факт, ибо как и в указанном примере, в проекте используется IoC контейнер, через который мы без труда можем запросить все то, что нам надо. Так что, я лично не стал бы использовать вот так локатор, но решать вам, стоит оно того, или нет.

А теперь покажу почему я считаю "локатор" избыточным
Возьмем за основу то, что у нас проект будет использовать IoC контейнер (в предложенном примере, это InstancesProvider, в моем случае это будет библиотека Microsoft.Extensions.DependencyInjection.

  • Создаем новый проект

  • Открываем App.xaml и убираем StartupUri="MainWindow.xaml"

  • Создаем класс для нашего окна, пусть будет стандартное MainViewModel

  • Открываем App.xaml.cs и переопределяем там OnStartup, прописывая в нем инициализацию контейнера, получение из него окна и VM, ну и открытие окна, получаем что-то такое:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
    
        var serviceProvider = new ServiceCollection()
            .AddTransient<MainViewModel>()
            .AddTransient<MainWindow>()
            .BuildServiceProvider();
    
        var window = serviceProvider.GetService<MainWindow>();
        var viewModel = serviceProvider.GetService<MainViewModel>();
    
        window!.DataContext = viewModel;
        window!.Show();
    }
    

    Это "топорная" реализация, по хорошему за "маппинг" должен отвечать отдельный сервис, который тут мы просим и через него открываем, но для примера (да и не только), думаю сойдет.

  • Запустим проект и увидим пустое окно. Поздравляю, мы сделали проект с IoC контейнером.

Теперь давайте подумаем про контент (страницы), пусть за них отвечает отдельный сервис, который будет оповещать слушателей о том, что страница изменилась.

  • Создаем интерфейс, который будет являться "контактом" для VM слоев страниц, пусть это будет простой и пустой interface IPage.

  • Теперь создадим еще один интерфейс, который будет описывать уже сам сервис, точней то, что "публичное", а именно метод изменения контента, да и событие, что-то такое:

    public interface IContentService
    {
        event Action<IPage>? OnPageChanged;
        void Change<TPage>() where TPage : IPage;
    }
    
  • Теперь реализуем сам сервис, максимально просто, что-то по типу этого:

    public class ContentService (IServiceProvider serviceProvider) : IContentService
    {
        public event Action<IPage>? OnPageChanged;
    
        public void Change<TPage>() where TPage : IPage
        {
            var page = serviceProvider.GetService<TPage>();
            if (page is null) return;
            OnPageChanged?.Invoke(page);
        }
    }
    
  • Регистрируем сервис в контейнер .AddSingleton<IContentService, ContentService>()

  • Теперь реализуем MainViewModel

    public partial class MainViewModel : ObservableObject
    {
        public MainViewModel(IContentService contentService) 
            => contentService.OnPageChanged += page => CurrentPage = page;
    
        [ObservableProperty]
        private IPage? _currentPage;
    }
    

    На ObservableObject и ObservableProperty внимание не обращайте, у вас тут может быть любая реализация INotifyPropertyChanged. В мое случае используется CommunityToolkit.MVVM.

  • В XAML окна пропишем <ContentPresenter Content="{Binding CurrentPage}" />, для примера этого достаточно.

И вот уже я думаю видно, какой простой у нас код, мы без каких либо ресурсов, статических объектов, создали сервис, внедрили его, и теперь где угодно можем использовать. А теперь взгляните на тот локатор и ответьте на вопрос "а зачем?"... Я вот лично на него ответить так и не смог)

Дальше пример я думаю смысла писать нету (если нужен, говорите, дополню), но в целом, просто создаем VM под нужные страницы и наследуем их от IPage и регистрируете каждую в контейнере, а также внедряем в нужную VM (да и не только) IContentService если нам требуется смена страницы, ну и вызываем метод Change() с нужным типом VM страницы для открытия (contentService.Change<LoginVM>();).

Собственно, это мои мысли на этот счет, думаю объяснил доходчиво почему мне не нравиться такие привязки, и почему я не очень люблю локаторы, особенно, имея под руками IoC контейнер...

→ Ссылка