Comments 12
В очередном проекте на 3 вкладки понадобилось менять доступность команд при изменении свойств, не являющихся параметрами этих команд. Вспомнил о старом друге и решил заодно поделиться им. Когда речь идёт о 500 строках кода — решение о добавлении новой зависимости для меня не является однозначным.
+1
Благодарю за статью, но по моему скромному мнению, как то всё вышеописанное необоснованно сложновато. Разве зависимость одного свойства от другого не реализуется в WPF через привязку свойство1.свойство2.свойство3 (и т.д.)? Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно. Нам остаётся только грамотно продумать объектную иерархию. На худой конец, когда надо отслеживать не связанные объекты, можно использовать MultiBinding, используя простейшие конверторы.
Да, с отображением доступности команд (действий), заморочек особенно много. Но тут проблема в отсутствии правильных библиотечных команд, которые должны содержать в себе нужную функциональность. Я для себя сам сделал такие (см. Инфраструктура команд для вызова пользователем действий в шаблоне MVVM) и практически забыл про проблему уведомления об изменении связанных свойств.
И ещё, заголовок статьи по моему слишком абстрактный. Хорошо бы добавить в него какую то главную мысль статьи.
Да, с отображением доступности команд (действий), заморочек особенно много. Но тут проблема в отсутствии правильных библиотечных команд, которые должны содержать в себе нужную функциональность. Я для себя сам сделал такие (см. Инфраструктура команд для вызова пользователем действий в шаблоне MVVM) и практически забыл про проблему уведомления об изменении связанных свойств.
И ещё, заголовок статьи по моему слишком абстрактный. Хорошо бы добавить в него какую то главную мысль статьи.
0
Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно.
Чего-то я здесь не понял, наверное. Речь идёт о зависимых и вычисляемых свойствах, ссылаясь на ту же исходную статью (сейчас я думаю, что можно было взять пример из неё ради сравнения), это, например, сумма заказа. Но иногда у нас есть агрегирующая ViewModel, которая зависит от внутренних VM. Это тоже пример вычисляемых свойств. Да, MultiBinding тоже есть, но под него всегда нужен конвертер и это переносит ближе к View нашу взаимосвязь. А View должна быть глупой.
С командами история отдельная и решений тоже достаточно. Видел даже патченный CompositeCommand из Prism, который делает RaiseCanExecuteChanged по движению мыши. Производительность — давай, до свидания.
+1
Существует ещё более лаконичный способ регистрации обработчиков:
Посмотрите эвокаторы свойств и команд в библиотеке Aero Framework, сама идея похожа на вашу.
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++;
}
}
+3
Стоит отметить, что эвокаторы свойств в 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";
+2
via
Работает через тот же AfterNotify. В этой статье я специально опустил подробности о ViewModelBase, Workspaces для управления ж.ц. отображений и прочие свистелки, сделав упор на INPC.
verifiableObject.ForProperty(vo => vo.TotalSize).AddValidationRule(s => s > 20);
verifiableObject.ForObject().AddAsyncValidationRule(ct => ValidateLengthAsync(ct)).AlsoValidate(vo => vo.TotalSize);
Работает через тот же AfterNotify. В этой статье я специально опустил подробности о ViewModelBase, Workspaces для управления ж.ц. отображений и прочие свистелки, сделав упор на INPC.
+2
Рекомендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать. Собственно, исторически в аэро фремворке сначала появились эвокаторы команд, а только потом свойств :)
Это крутая фича для тех, у кого страсть к простоте и лаконичности кода.
this[MediaCommands.Play].CanExecute += (sender, args) => args.CanExecute = IsStopped;
this[Context.Get("HelloCommand")].Executed += (sender, args) => MessageBox.Show("Hello!");
Это крутая фича для тех, у кого страсть к простоте и лаконичности кода.
+1
Ок, раз речь зашла о командах. Вот такая реализация в Rikrop:
Можно добавлять сколько угодно много CanExecute, проверяться будут все, можно добавлять инвалидацию по изменению какого-либо свойства текущего объекта или другого объекта (а как это делается у вас?), можно добавлять блокировку по изменению состояния валидируемого объекта (см. выше), можно добавлять блокировку по состоянию IBusyItem — это такая обёртка над всем, что может блокировать что-либо (долгоиграющие операции).
Инвалидация от произвольных свойств нужна довольно часто, по состоянию валидации модели тоже приходится изменять доступность команд. Так что рекомаендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать.
Нет больше сил читать слово эвокатор — каждый раз перед глазами одно и то же:
MyCommand = new RelayCommandBuilder(p => DoSomeWork(p)).AddCanExecute(p => p.ReadyToWork).InvalidateOnNotify(_myDependency, md => md.SomeProperty).AddBlocker(_myServiceExecutor).CreateCommand();
Можно добавлять сколько угодно много CanExecute, проверяться будут все, можно добавлять инвалидацию по изменению какого-либо свойства текущего объекта или другого объекта (а как это делается у вас?), можно добавлять блокировку по изменению состояния валидируемого объекта (см. выше), можно добавлять блокировку по состоянию IBusyItem — это такая обёртка над всем, что может блокировать что-либо (долгоиграющие операции).
Инвалидация от произвольных свойств нужна довольно часто, по состоянию валидации модели тоже приходится изменять доступность команд. Так что рекомаендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать.
Нет больше сил читать слово эвокатор — каждый раз перед глазами одно и то же:
+2
Ок, забудем это слово :)
Инвалидация контекстных команд происходит, например, следующим образом по изменению свойства:
Преимущество такого способа в сравнении с вашим в том, что легко можно добавить условную логику в этот процесс, а не только ориентироваться на само событие изменения свойства, вот что имею в виду:
Поскольку в некоторых случаях обработчик CanExecute вызывать затратно или излишне.
Контекстные команды очень похожи в использовании на RoutedCommands из WPF, по сути, это их упрощённая кроссплатформенная модель.
Интересно, у вас можно сделать что-то наподобие такого?
Имею в виду асинхронность async/await.
Инвалидация контекстных команд происходит, например, следующим образом по изменению свойства:
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.
0
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 движок.
0
И ещё вопрос, допускаются ли в вашей реализации асинхронные обработчики у событий нотификации?
Иногда очень полезно иметь такую возможность.
this[() => Text].PropertyChanged += async (sender, args) =>
{
IsSaved = await server.SaveText(Text);
};
Иногда очень полезно иметь такую возможность.
0
Sign up to leave a comment.
Базовая реализация INotifyPropertyChanged