Как лучше подключаться к команде со стороны XAML?
Обычно я делаю привязку вот так вот - Command="{Binding SomeCommand}", где SomeCommand это команда из DataContext.
Но вот в этом учебном проекте https://github.com/xellans/LearnMvvm.git я столкнулся с необычным для меня вариантом
<RadioButton Command="{local:SetCurrentContext}" CommandParameter="PersonVM" Content="{DynamicResource Lang1}"></RadioButton>
Приемлемый ли это способ привязки к командам? И какой способ считается более правильным?
Ответы (1 шт):
Для того, чтобы данная привязка работала, ваша команда (и любой другой объект для привязки) должна быть статичная, открываем код, смотрим, и действительно:
public static readonly RoutedUICommand SetCurrentContext = new RoutedUICommand(
"Задание текущего контекста навигатору, находящемуся в ресурсах, вызвашего команду, элемента.",
"SetCurrentContext",
typeof(NavigatorLocator));
Ну а статика зло, ибо как минимум
- Статика плохо тестируется
- Статика вечно висит в памяти даже тогда, когда нам этот объект вовсе не нужен
- Ну и др., тонкости, которые надо учитывать
Я не говорю, что у вас не должно быть статики, я говорю, что лучше обходиться без нее, если есть такая возможность, а в этом конкретном случае, такая возможность есть.
А теперь подумайте, как вы будете реализовать другие свои команды для привязки вида {local:SetCurrentContext}
? Плодить тонну статичных объектов? Думаю
и сами понимаете, что это глупое решение.
Так что, для единичного использования, как в примере, локатора (о нем поговорим чуть позже), такой подход может и имеет смысл, но делать все свойства для привязки статичными, это стрелять себе в ногу. Если более детальней изучить код из примера, то увидите там дальше нормальные команды и нормальные привязки, например, AuthUserControl.xaml
, в нем есть Command="{Binding AuthorizeCommand}"
, а в VM уже стандартное, не статичное свойство public RelayCommand AuthorizeCommand { get; }
.
Теперь что касается локатора
Скажу честно, мне данный подход не нравиться по нескольким причинам:
- Статика - как и писал выше, я против нее, а локаторы зачастую на ней основаны.
- Локаторы часто внедряются в ресурсы (View слой), и именно XAML им руководит (создает, хранит, управляет сроком жизни, и т.д.), а это значит, что мы теряем полный контроль над ним, что очень плохо. Также подобное плодит такие костыли, как в показанном примере (файл
App.xaml.cs
->(NavigatorLocator)this.FindResource("locator");
). - Код сильно усложняется ради того, что можно сделать куда проще.
Локатор хорош тогда, когда у вас очень большой проект, и то не факт, ибо как и в указанном примере, в проекте используется 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 контейнер...