Как стать автором
Обновить

Комментарии 12

В очередном проекте на 3 вкладки понадобилось менять доступность команд при изменении свойств, не являющихся параметрами этих команд. Вспомнил о старом друге и решил заодно поделиться им. Когда речь идёт о 500 строках кода — решение о добавлении новой зависимости для меня не является однозначным.
Благодарю за статью, но по моему скромному мнению, как то всё вышеописанное необоснованно сложновато. Разве зависимость одного свойства от другого не реализуется в WPF через привязку свойство1.свойство2.свойство3 (и т.д.)? Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно. Нам остаётся только грамотно продумать объектную иерархию. На худой конец, когда надо отслеживать не связанные объекты, можно использовать MultiBinding, используя простейшие конверторы.
Да, с отображением доступности команд (действий), заморочек особенно много. Но тут проблема в отсутствии правильных библиотечных команд, которые должны содержать в себе нужную функциональность. Я для себя сам сделал такие (см. Инфраструктура команд для вызова пользователем действий в шаблоне MVVM) и практически забыл про проблему уведомления об изменении связанных свойств.
И ещё, заголовок статьи по моему слишком абстрактный. Хорошо бы добавить в него какую то главную мысль статьи.
Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно.

Чего-то я здесь не понял, наверное. Речь идёт о зависимых и вычисляемых свойствах, ссылаясь на ту же исходную статью (сейчас я думаю, что можно было взять пример из неё ради сравнения), это, например, сумма заказа. Но иногда у нас есть агрегирующая ViewModel, которая зависит от внутренних VM. Это тоже пример вычисляемых свойств. Да, MultiBinding тоже есть, но под него всегда нужен конвертер и это переносит ближе к View нашу взаимосвязь. А View должна быть глупой.
С командами история отдельная и решений тоже достаточно. Видел даже патченный CompositeCommand из Prism, который делает RaiseCanExecuteChanged по движению мыши. Производительность — давай, до свидания.
Существует ещё более лаконичный способ регистрации обработчиков:

this[() => Text].PropertyChanging += (o, args) => { ... };
this[() => Text].PropertyChanged += (o, args) => { ... };

Посмотрите эвокаторы свойств и команд в библиотеке Aero Framework, сама идея похожа на вашу.

Пример простой вьюмодели
    [DataContract]
    public class GuyViewModel : ContextObject, IExposable
    {
        [DataMember]
        public int Kisses
        {
            get { return Get(() => Kisses); }
            set { Set(() => Kisses, value); }
        }

        public void Expose()
        {
            var girlViewModel = Store.Get<GirlViewModel>();

            this[() => Kisses].PropertyChanged += (sender, args) =>
            {
                Context.Get("KissGirl").RaiseCanExecuteChanged();
                Context.Get("KissGuy").RaiseCanExecuteChanged();
            };

            this[Context.Get("KissGirl")].CanExecute += (sender, args) => 
                args.CanExecute = Kisses > girlViewModel.Kisses - 2;

            this[Context.Get("KissGirl")].Executed += (sender, args) => 
                girlViewModel.Kisses++;
        }
    }



Стоит отметить, что эвокаторы свойств в Aero Framework также поддерживают достаточно удобные способы как синхронной валидации свойств (IDataErrorInfo), так и асинхронной (INotifyDataErrorInfo). Выглядит подобным образом:

this[() => Mouse].ErrorsChanged += (sender, args) => HasErrors = !(5 < Mouse.Length && Mouse.Length < 20);
this[() => Mouse].ValidationRules += s => 5 < Mouse.Length && Mouse.Length < 20 ? null : "Invalid Length";
via
verifiableObject.ForProperty(vo => vo.TotalSize).AddValidationRule(s => s > 20);
verifiableObject.ForObject().AddAsyncValidationRule(ct => ValidateLengthAsync(ct)).AlsoValidate(vo => vo.TotalSize);

Работает через тот же AfterNotify. В этой статье я специально опустил подробности о ViewModelBase, Workspaces для управления ж.ц. отображений и прочие свистелки, сделав упор на INPC.
Рекомендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать. Собственно, исторически в аэро фремворке сначала появились эвокаторы команд, а только потом свойств :)

this[MediaCommands.Play].CanExecute += (sender, args) => args.CanExecute = IsStopped;
this[Context.Get("HelloCommand")].Executed += (sender, args) => MessageBox.Show("Hello!");

Это крутая фича для тех, у кого страсть к простоте и лаконичности кода.
Ок, раз речь зашла о командах. Вот такая реализация в Rikrop:
MyCommand = new RelayCommandBuilder(p => DoSomeWork(p)).AddCanExecute(p => p.ReadyToWork).InvalidateOnNotify(_myDependency, md => md.SomeProperty).AddBlocker(_myServiceExecutor).CreateCommand();

Можно добавлять сколько угодно много CanExecute, проверяться будут все, можно добавлять инвалидацию по изменению какого-либо свойства текущего объекта или другого объекта (а как это делается у вас?), можно добавлять блокировку по изменению состояния валидируемого объекта (см. выше), можно добавлять блокировку по состоянию IBusyItem — это такая обёртка над всем, что может блокировать что-либо (долгоиграющие операции).

Инвалидация от произвольных свойств нужна довольно часто, по состоянию валидации модели тоже приходится изменять доступность команд. Так что рекомаендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать.

Нет больше сил читать слово эвокатор — каждый раз перед глазами одно и то же:
image
Ок, забудем это слово :)

Инвалидация контекстных команд происходит, например, следующим образом по изменению свойства:

            this[() => Kisses].PropertyChanged += (sender, args) =>
            {
                Context.Get("KissGirl").RaiseCanExecuteChanged();
                Context.Get("KissGuy").RaiseCanExecuteChanged();
            };

Преимущество такого способа в сравнении с вашим в том, что легко можно добавить условную логику в этот процесс, а не только ориентироваться на само событие изменения свойства, вот что имею в виду:

            this[() => Kisses].PropertyChanged += (sender, args) =>
            {
                if (Kesses < 10) return;
                Context.Get("KissGirl").RaiseCanExecuteChanged();
                Context.Get("KissGuy").RaiseCanExecuteChanged();
            };

Поскольку в некоторых случаях обработчик CanExecute вызывать затратно или излишне.

Контекстные команды очень похожи в использовании на RoutedCommands из WPF, по сути, это их упрощённая кроссплатформенная модель.

Интересно, у вас можно сделать что-то наподобие такого?

this[Context.Refresh].Executed += async (sender, args) =>
{
      try
      {
            IsBusy = true;
            User = await server.GetUserData(Login, Password);
      }
      finally
      {
            IsBusy = false;
      }
};

Имею в виду асинхронность async/await.
AfterNotify(() => Kisses).Execute(() => 
{
    if (Kesses < 10) return;
    _myFirstCommand.RaiseCanExecuteChanged();
    _anotherOneCommand.RaiseCanExecuteChanged();
});

GetUserCommand = new RelayCommandBuilder<int>(async id => User  await userServiceExecutor.Execute(us => us.GetUser(id))).CreateCommand();

AfterNotify(() => Scene.Dimension).Execute(async () => await OnCombine(Scene.Dimension));

Предлагаю ничью пока вы не расчехлили сохранение состояния конфигурации, а я MapReduce движок.
Добро, согласен на ничью )
Довольно интересно наблюдать, как получают развитие некоторые схожие идеи, но разными путями.
Можете в двух словах описать, что умеет ваш MapReduce движок?
И ещё вопрос, допускаются ли в вашей реализации асинхронные обработчики у событий нотификации?

this[() => Text].PropertyChanged += async (sender, args) =>
{
       IsSaved = await server.SaveText(Text); 
};

Иногда очень полезно иметь такую возможность.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Публикации

Истории