Как приручить реактивное
программирование в XAML приложениях
Денис Цветцих
Ice Rock Dev
icerockdev.com
DotNext Spb
3 июня 2016
dotnext.ru/spb/
2
Кто я?
• 7+ лет .NET
• Разработка корпоративных приложений
• Люблю хипстерские библиотеки 
3
Почему я здесь?
• 6 лет назад приручил Rx
• 3 года назад приручил ReactiveUI
4
ReactiveUI – ни одного поста на хабре
5
Сэмплы не показательны
Rx – 101 пример задач, где можно обойтись без Rx
ReactiveUI – единственный жизненный сэмпл – поиск (на
главной странице)
6
О чем мы поговорим?
• Что такое реактивное программирование
• Примеры задач из продакшен проектов, которые проще
решаются с помощью Rx и ReactiveUI
• Антипаттерны: как ReactiveUI лучше не использовать
ЧТО ТАКОЕ РЕАКТИВНОЕ ПРОГРАММИРОВАНИЕ
8
Что такое реактивное
программирование?
Реактивное программирование – это парадигма
программирования, ориентированная на потоки данных и
распространение изменений.
(Википедия)
9
Парадигма программирования
Парадигма программирования – это система идей и
понятий, определяющих стиль написания компьютерных
программ.
10
Императивная программа
var A = 10;
var B = A + 1;
// Чему равно В?
11
A = A + 1;
// А теперь чему равно В?
11
11
Реактивная программа
var A = 10;
var B <- A + 1;
<- оператор «судьбы»
// Чему равно В?
11
A = A + 1;
// А теперь чему равно В?
12
12
Реактивное программирование
Реактивное программирование – это парадигма
программирования, ориентированная на потоки данных и
распространение изменений.
Поток данных – последовательность значений переменной
или свойства класса.
Распространение изменений – уведомления
«заинтересованных» об изменениях.
13
Pull
Pull – класс A взаимодействует с классом B и вытягивает из
него необходимые данные
Class A Class B
GetData()
Interacts with
14
Push
Push – класс B самостоятельно выталкивает данные классу
A, как только они становятся доступны.
Class A
ProcessData()
Class BReacts on
РЕАКТИВНОЕ ПРОГРАММИРОВАНИЕ НА C#
16
Реактивное программирование на .NET
• Reactive Extensions (Rx)
• Эрик Мейер из MS Research
• ReactiveUI
• Разработана в MS Research
• библиотека на базе Rx для создания элегантных UI для всех
XAML платформ
17
Внутри .NET
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<T>
{
void OnNext(T value);
void OnCompleted();
void OnError(Exception error);
}
18
Внутри .NET
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<T>
{
void OnNext(T value);
void OnCompleted();
void OnError(Exception error);
}
19
Внутри .NET
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<T>
{
void OnNext(T value);
void OnCompleted();
void OnError(Exception error);
}
20
Внутри .NET
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<T>
{
void OnNext(T value);
void OnCompleted();
void OnError(Exception error);
}
21
Внутри .NET
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
public interface IObserver<T>
{
void OnNext(T value);
void OnCompleted();
void OnError(Exception error);
}
22
Оператор судьбы через Rx
var A = new Subject<int>();
// B <- A + 1
var B = A.Select(a => a + 1);
A.OnNext(10);
23
Оператор судьбы через Rx
var A = new Subject<int>();
// B <- A + 1
var B = A.Select(a => a + 1);
A.OnNext(10);
24
Оператор судьбы через Rx
var A = new Subject<int>();
// B <- A + 1
var B = A.Select(a => a + 1);
A.OnNext(10);
25
Оператор судьбы через Rx
var A = new Subject<int>();
// B <- A + 1
var B = A.Select(a => a + 1);
A.OnNext(10);
26
Observable.Select – проекция
27
Observable.Merge – слияние
КАКИЕ ЗАДАЧИ МОЖНО РЕШАТЬ
29
Пример
Приложение для поиска по материалам конференции DotNext
30
Для читабельности примеров
public class Filter : ReactiveObject
{
private DateTime _fromDate;
private DateTime _toDate;
public DateTime FromDate
{
get { return _fromDate; }
set { this.RaiseAndSetIfChanged(ref _fromDate, value); }
}
public DateTime ToDate
{
get { return _toDate; }
set { this.RaiseAndSetIfChanged(ref _toDate, value); }
}
}
31
Для читабельности примеров
public class Filter : ReactiveObject
{
private DateTime _fromDate;
private DateTime _toDate;
public DateTime FromDate
{
get { return _fromDate; }
set { this.RaiseAndSetIfChanged(ref _fromDate, value); }
}
public DateTime ToDate
{
get { return _toDate; }
set { this.RaiseAndSetIfChanged(ref _toDate, value); }
}
}
32
Для читабельности примеров
public class Filter : ReactiveObject
{
private DateTime _fromDate;
private DateTime _toDate;
public DateTime FromDate
{
get { return _fromDate; }
set { this.RaiseAndSetIfChanged(ref _fromDate, value); }
}
public DateTime ToDate
{
get { return _toDate; }
set { this.RaiseAndSetIfChanged(ref _toDate, value); }
}
}
33
Для читабельности примеров
public class Filter
{
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }
}
34
Задача 1: подписка на события об изменении
свойств фильтра
35
Обычная подписка на событие PropertyChanged
var filter = new Filter();
// Подписка
filter.PropertyChanged += OnPropertyChanged;
// Реакция на событие
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs e)
{
}
// Отписка
filter.PropertyChanged -= OnPropertyChanged;
36
Обычная подписка на событие PropertyChanged
var filter = new Filter();
// Подписка
filter.PropertyChanged += OnPropertyChanged;
// Реакция на событие
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs e)
{
}
// Отписка
filter.PropertyChanged -= OnPropertyChanged;
37
var filter = new Filter();
// Подписка
filter.PropertyChanged += OnPropertyChanged;
// Реакция на событие
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs e)
{
}
// Отписка
filter.PropertyChanged -= OnPropertyChanged;
Обычная подписка на событие PropertyChanged
38
Обычная подписка на событие PropertyChanged
var filter = new Filter();
// Подписка
filter.PropertyChanged += OnPropertyChanged;
// Реакция на событие
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs e)
{
}
// Отписка
filter.PropertyChanged -= OnPropertyChanged;
39
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
Rx подписка на событие PropertyChanged
40
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
Rx подписка на событие PropertyChanged
41
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
Rx подписка на событие PropertyChanged
42
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
Rx подписка на событие PropertyChanged
43
Rx подписка на событие PropertyChanged
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
44
Rx подписка на событие PropertyChanged
filter = new Filter();
// Подписка
IDisposable subscription =
Observable.FromEventPattern<PropertyChangedEventArgs>
(filter, "PropertyChanged")
.Subscribe(OnPropertyChanged);
// Реакция на событие
private void OnPropertyChanged
(EventPattern<PropertyChangedEventArgs> eventPattern)
{
//eventPattern.Sender и eventPattern.EventArgs
}
// Отписка
subscription.Dispose();
45
Достоинства Rx подписки
• Подписка инкапсулируется в IDisposable объекте, просто
сделать отписку
• Подписка – это поток данных, над которым можно
выполнять операции
46
Задача 2: подписка на изменение свойства
FromDate
47
Обычная подписка на изменение свойства FromDate
Filter filter = new Filter();
filter.PropertyChanged += OnPropertyChanged;
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == “FromDate")
{
// Действия
}
}
48
Обычная подписка на изменение свойства FromDate
Filter filter = new Filter();
filter.PropertyChanged += OnPropertyChanged;
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == “FromDate")
{
// Действия
}
}
49
Обычная подписка на изменение свойства FromDate
Filter filter = new Filter();
filter.PropertyChanged += OnPropertyChanged;
private void OnPropertyChanged
(object sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == “FromDate")
{
// Действия
}
}
50
ReactiveUI подписка на изменение свойства FromDate
var filter = new Filter();
var subscription = filter
.ObservableForProperty(f => f.FromDate)
.Subscribe(FromDateChanged);
private void FromDateChanged
(IObservedChange<Filter, DateTime> change)
{
// Действия
}
51
ReactiveUI подписка на изменение свойства FromDate
var filter = new Filter();
var subscription = filter
.ObservableForProperty(f => f.FromDate)
.Subscribe(FromDateChanged);
private void FromDateChanged
(IObservedChange<Filter, DateTime> change)
{
// Действия
}
52
ReactiveUI подписка на изменение свойства FromDate
var filter = new Filter();
var subscription = filter
.ObservableForProperty(f => f.FromDate)
.Subscribe(FromDateChanged);
private void FromDateChanged
(IObservedChange<Filter, DateTime> change)
{
// Действия
}
53
ReactiveUI подписка на изменение свойства FromDate
var filter = new Filter();
var subscription = filter
.ObservableForProperty(f => f.FromDate)
.Subscribe(FromDateChanged);
private void FromDateChanged
(IObservedChange<Filter, DateTime> change)
{
// Действия
}
54
Достоинства ObservableForProperty
• Не нужно проверять, что обрабатываем событие об
изменении нужного свойства
55
Задача 3: действия при изменении значений
свойств
56
Добавим выбор страны и города
Добавим в фильтр:
• Список стран и городов
• При выборе страны актуализируем список городов
57
Валидация выбранного периода
Условие: FromDate не превосходит ToDate
Если FromDate > ToDate, тогда ToDate приравниваем к
FromDate
Если ToDate < FromDate, тогда FromDate приравниваем к
ToDate
58
Обычное решение
private DateTime _fromDate;
public DateTime FromDate
{
get { return _fromDate; }
set
{
_fromDate = value;
OnPropertyChanged();
if (_fromDate > ToDate) ToDate = _fromDate;
}
}
59
Обычное решение
private DateTime _fromDate;
public DateTime FromDate
{
get { return _fromDate; }
set
{
_fromDate = value;
OnPropertyChanged();
if (_fromDate > ToDate) ToDate = _fromDate;
}
}
60
ObservableForProperty
private IDisposable _subscription;
public Filter()
{
_subscription =
this.ObservableForProperty(vm => vm.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
}
public DateTime FromDate { get; set; } // PropertyChanged
61
ObservableForProperty
private IDisposable _subscription;
public Filter()
{
_subscription =
this.ObservableForProperty(vm => vm.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
}
public DateTime FromDate { get; set; } // PropertyChanged
62
ObservableForProperty
private IDisposable _subscription;
public Filter()
{
_subscription =
this.ObservableForProperty(vm => vm.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
}
public DateTime FromDate { get; set; } // PropertyChanged
63
ObservableForProperty
private IDisposable _subscription;
public Filter()
{
_subscription =
this.ObservableForProperty(vm => vm.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
}
public DateTime FromDate { get; set; } // PropertyChanged
64
ObservableForProperty
private IDisposable _subscription;
public Filter()
{
_subscription =
this.ObservableForProperty(vm => vm.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
}
public DateTime FromDate { get; set; } // PropertyChanged
if (_fromDate > ToDate) ToDate = _fromDate;
65
Зависимости между свойствами в обычном решении
public class Filter
{
public DateTime FromDate
{
get { return _fromDate; }
set
{
_fromDate = value;
if (_fromDate > ToDate) ToDate = FromDate;
}
}
public DateTime ToDate
{
get { return _toDate; }
set
{
_toDate = value;
if (_toDate < FromDate) FromDate = _toDate;
}
}
public List<string> Cities
{
get { return _cities; }
set
{
_cities = value;
SelectedCity = null;
}
}
public string SelectedCountry
{
get { return _selectedCountry; }
set
{
_selectedCountry = value;
Cities = GetCitesForCountry(_selectedCountry);
}
}
}
66
Зависимости между свойствами в ReactiveUI решении
public class Filter
{
public Filter()
{
_fromDateSubscription =
this.ObservableForProperty(filter => filter.FromDate)
.Where(fromDate => fromDate.Value > ToDate)
.Subscribe(fromDate => ToDate = fromDate.Value);
_toDateSubscription =
this.ObservableForProperty(filter => filter.ToDate)
.Where(toDate => toDate.Value < FromDate)
.Subscribe(toDate => FromDate = toDate.Value);
_selectedCountrySubscription =
this.ObservableForProperty(filter => filter.SelectedCountry)
.Subscribe(country => Cities = GetCitesForCountry(country.Value));
_citiesSubscription = this.ObservableForProperty(filter => Cities)
.Subscribe(_ => SelectedCity = null);
}
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }
public List<string> Countries { get; set; }
public List<string> Cities { get; set; }
public string SelectedCountry { get; set; }
public string SelectedCity { get; set; }
}
67
Достоинства ObservableForProperty
• Удаление логики из сеттеров
• Делает код на порядок более читаемым, собирая код
зависимости между свойствами в конструкторе
68
Задача 4: мониторинг одновременно нескольких
свойств
Поля фильтра:
• От
• До
• Страна
• Город
При изменении любого – обновляем данные
69
ViewModel с фильтром
public class ViewModel
{
public Filter Filter { get; set; }
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
}
}
70
ViewModel с фильтром
public class ViewModel
{
public Filter Filter { get; set; }
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
}
}
71
ViewModel с фильтром
public class ViewModel
{
public Filter Filter { get; set; }
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
}
}
72
Обычное решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
Filter.PropertyChanged += OnFilterPropertyChanged;
}
private void OnFilterPropertyChanged
(object sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == "FromDate" ||
eventArgs.PropertyName == "ToDate" ||
eventArgs.PropertyName == "SelectedCountry" ||
eventArgs.PropertyName == "SelectedCity")
{
Refresh();
}
}
73
Обычное решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
Filter.PropertyChanged += OnFilterPropertyChanged;
}
private void OnFilterPropertyChanged
(object sender, PropertyChangedEventArgs eventArgs)
{
if (eventArgs.PropertyName == "FromDate" ||
eventArgs.PropertyName == "ToDate" ||
eventArgs.PropertyName == "SelectedCountry" ||
eventArgs.PropertyName == "SelectedCity")
{
Refresh();
}
}
74
ReactiveUI решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
_subscription =
Filter.WhenAnyValue(
f => f.FromDate,
f => f.ToDate,
f => f.SelectedCountry,
f => f.SelectedCity)
.Subscribe(_ => Refresh());
}
75
ReactiveUI решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
_subscription =
Filter.WhenAnyValue(
f => f.FromDate,
f => f.ToDate,
f => f.SelectedCountry,
f => f.SelectedCity)
.Subscribe(_ => Refresh());
}
76
ReactiveUI решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
_subscription =
Filter.WhenAnyValue(
f => f.FromDate,
f => f.ToDate,
f => f.SelectedCountry,
f => f.SelectedCity)
.Subscribe(_ => Refresh());
}
77
ReactiveUI решение
public ViewModel()
{
Filter = new Filter();
// Подписка на изменение свойств фильтра
_subscription =
Filter.WhenAnyValue(
f => f.FromDate,
f => f.ToDate,
f => f.SelectedCountry,
f => f.SelectedCity)
.Subscribe(_ => Refresh());
}
78
Достоинства WhenAnyValue
• Подписываемся только на нужные нам свойства
• Можно подписываться на изменения вложенных свойств
• Obj.Deep1.Deep2.Deep3…
79
Задача 5: проверка CanExecute
команды
• Добавляем кнопку «Обновить» на фильтр
• Страна и город – обязательные поля
80
RelayCommand из MvvmLight
public RelayCommand RefreshCommand { get; private set; }
public Filter()
{
RefreshCommand = new RelayCommand(
OnRefresh,
() => SelectedCity != null && SelectedCountry != null);
}
81
RelayCommand из MvvmLight
public RelayCommand RefreshCommand { get; private set; }
public Filter()
{
RefreshCommand = new RelayCommand(
OnRefresh,
() => SelectedCity != null && SelectedCountry != null);
}
82
RelayCommand из MvvmLight
public RelayCommand RefreshCommand { get; private set; }
public Filter()
{
RefreshCommand = new RelayCommand(
OnRefresh,
() => SelectedCity != null && SelectedCountry != null);
}
83
RelayCommand из MvvmLight
public RelayCommand RefreshCommand { get; private set; }
public Filter()
{
RefreshCommand = new RelayCommand(
OnRefresh,
() => SelectedCity != null && SelectedCountry != null);
}
84
Подергивания 
public string SelectedCountry {
get { return _selectedCountry; }
set {
_selectedCountry = value; OnPropertyChanged();
RefreshCommand.RaiseCanExecuteChanged();
}
}
public string SelectedCity {
get { return _selectedCity; }
set {
_selectedCity = value; OnPropertyChanged();
RefreshCommand.RaiseCanExecuteChanged();
}
}
85
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
86
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
87
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
88
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
89
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
90
ReactiveCommand
public Filter()
{
var canExecute = this.WhenAny(
f => f.SelectedCity,
f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.Create(canExecute);
command.Subscribe(_ => OnRefresh());
RefreshCommand = command;
}
public string SelectedCountry { get; set; }
// RefreshCommand.RaiseCanExecuteChanged();
91
Достоинства ReactiveCommand
• Не нужны «подергивания» в сеттерах
• При изменения условия нельзя забыть убрать или
добавить «подергивание»
92
Задача 6: команда запускает асинхронное
действие
• При выполнении команды – вызов вебсервиса
• Пока не получим ответ от сервиса – команду нельзя
выполнить
93
Асинхронное действие RelayCommand
public bool IsBusy { get; set; }
//RefreshCommand.RaiseCanExecuteChanged();
public Filter(Task refreshTask)
{
RefreshTask = refreshTask;
RefreshCommand = new RelayCommand(OnRefresh,
() => SelectedCity != null &&
SelectedCountry != null &&
!IsBusy);
}
94
Асинхронное действие RelayCommand
public bool IsBusy { get; set; }
//RefreshCommand.RaiseCanExecuteChanged();
public Filter(Task refreshTask)
{
RefreshTask = refreshTask;
RefreshCommand = new RelayCommand(OnRefresh,
() => SelectedCity != null &&
SelectedCountry != null &&
!IsBusy);
}
95
Асинхронное действие RelayCommand
public bool IsBusy { get; set; }
//RefreshCommand.RaiseCanExecuteChanged();
public Filter(Task refreshTask)
{
RefreshTask = refreshTask;
RefreshCommand = new RelayCommand(OnRefresh,
() => SelectedCity != null &&
SelectedCountry != null &&
!IsBusy);
}
96
Асинхронное обновление
private async void OnRefresh()
{
IsBusy = true;
await RefreshTask; // запрос на сервер
IsBusy = false;
}
97
Асинхронное обновление
private async void OnRefresh()
{
IsBusy = true;
await RefreshTask; // запрос на сервер
IsBusy = false;
}
98
Асинхронное обновление
private async void OnRefresh()
{
IsBusy = true;
await RefreshTask; // запрос на сервер
IsBusy = false;
}
99
ReactiveAsyncCommand
public Filter(Func<object, Task> refreshFunc)
{
var canExecute = this.WhenAny(
f => f.SelectedCity, f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.CreateAsyncTask(
canExecute,
refreshFunc);
RefreshCommand = command;
}
100
ReactiveAsyncCommand
public Filter(Func<object, Task> refreshFunc)
{
var canExecute = this.WhenAny(
f => f.SelectedCity, f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.CreateAsyncTask(
canExecute,
refreshFunc);
RefreshCommand = command;
}
101
ReactiveAsyncCommand
public Filter(Func<object, Task> refreshFunc)
{
var canExecute = this.WhenAny(
f => f.SelectedCity, f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.CreateAsyncTask(
canExecute,
refreshFunc);
RefreshCommand = command;
}
102
ReactiveAsyncCommand
public Filter(Func<object, Task> refreshFunc)
{
var canExecute = this.WhenAny(
f => f.SelectedCity, f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.CreateAsyncTask(
canExecute,
refreshFunc);
RefreshCommand = command;
}
103
ReactiveAsyncCommand
public Filter(Func<object, Task> refreshFunc)
{
var canExecute = this.WhenAny(
f => f.SelectedCity, f => f.SelectedCountry,
(city, country) =>
city.Value != null && country.Value != null);
var command = ReactiveCommand.CreateAsyncTask(
canExecute,
refreshFunc);
RefreshCommand = command;
}
104
Демо
105
Достоинства ReactiveAsyncCommand
• Не нужно отдельного свойства для мониторинга
начала/конца операции
106
Задача 7: поиск по подстроке
Вместо фильтра делаем поиск
Если длина строки < 3, то ждем 0,5 сек перед поиском
Если длина строки >= 3, то ждем 0,25 сек перед поиском
107
Поиск по строке
public class ViewModel
{
public Filter Filter { get; set; }
public string SearchText { get; set; }
}
108
Поиск по строке
public class ViewModel
{
public Filter Filter { get; set; }
public string SearchText { get; set; }
}
109
Поиск по строке
public class ViewModel
{
public Filter Filter { get; set; }
public string SearchText { get; set; }
}
110
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
111
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
112
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
113
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
114
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
115
Where, Throttle
var textObservable =
this.ObservableForProperty(s => s.SearchText);
var firstRule = textObservable
.Where(text => text.Value == null || text.Value.Length < 3)
.Throttle(TimeSpan.FromMilliseconds(500));
var secondRule = textObservable
.Where(text => text.Value != null && text.Value.Length >= 3)
.Throttle(TimeSpan.FromMilliseconds(250));
116
Merge, ObserveOnDispatcher
var resultObservable =
firstRule.Merge(secondRule);
resultObservable
.ObserveOnDispatcher()
.Subscribe(Refresh);
117
Merge, ObserveOnDispatcher
var resultObservable =
firstRule.Merge(secondRule);
resultObservable
.ObserveOnDispatcher()
.Subscribe(Refresh);
118
Merge, ObserveOnDispatcher
var resultObservable =
firstRule.Merge(secondRule);
resultObservable
.ObserveOnDispatcher()
.Subscribe(Refresh);
119
Достоинства
• На порядок меньшее кода, чем написали бы для
обычного решения
• Код простой и читаемый
АНТИПАТТЕРН: REACTIVEUI КАК MVVM
121
View – ViewModel маппинг
private void RegisterParts(IMutableDependencyResolver dependencyResolver)
{
dependencyResolver.RegisterConstant(this, typeof(IScreen));
dependencyResolver.Register(
() => new WelcomeView(), typeof(IViewFor<WelcomeViewModel>));
}
122
View – ViewModel маппинг
private void RegisterParts(IMutableDependencyResolver dependencyResolver)
{
dependencyResolver.RegisterConstant(this, typeof(IScreen));
dependencyResolver.Register(
() => new WelcomeView(), typeof(IViewFor<WelcomeViewModel>));
}
123
ViewModel First навигация
var viewModel =
(ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel));
viewModel.Title = "New ViewModel";
Router.Navigate.Execute(viewModel);
124
ViewModel First навигация
var viewModel =
(ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel));
viewModel.Title = "New ViewModel";
Router.Navigate.Execute(viewModel);
125
ViewModel First навигация
var viewModel =
(ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel));
viewModel.Title = "New ViewModel";
Router.Navigate.Execute(viewModel);
126
ViewModel First навигация
var viewModel =
(ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel));
viewModel.Title = "New ViewModel";
Router.Navigate.Execute(viewModel);
127
Нет CompositeUI
<Grid UseLayoutRounding="True">
<!--
COOLSTUFF: RoutedViewHost
RoutedViewHost will take a Router and display whatever ViewModel
is at the front of the route stack.
-->
<rx:RoutedViewHost
Router="{Binding Router}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
</Grid>
128
Нельзя избавиться от Splat IoC
В Splat
• Получить экземпляр сервиса – аналог ServiceLocator
Locator.CurrentMutable.GetService(typeof(Service))
Выход
• Реализовать IMutableDependencyResolver (для AutoFac)
129
Недостатки ReactiveUI как MVVM
• Нельзя задать конвенцию именования для маппинга View-
ViewModel
• Нет API, упрощающего задание соответствия View-ViewModel
• Splat – это ServiceLocator в чистом виде
• Нельзя заменить Splat на свой любимый IoC
• Нельзя реализовать CompositeUI
• ViewModelFirst навигация неудобная для UWP, WinRT, Windows
Phone
ЗАКЛЮЧЕНИЕ
131
Достоинства ReactiveUI
• Меньше инфраструктурного кода
• Повышает читаемость кода, группируя зависимости между
свойствами в конструкторе
• Решение типовых задач
• Подписка на изменение свойства
• Подписка на изменение нескольких свойств
• Автоматическая проверка состояния команды при:
• Изменении любого свойства, входящего в условие
• Начале/окончании асинхронной операции, запускаемой командой
132
Главное достоинство
Упрощается решение типовых задач разработчика XAML
приложений, а не какой-то экзотики!
133
Недостатки Rx и ReactiveUI
• Требует аккуратного использования, написать что-то
нечитаемое просто
• ReactiveUI лучше использовать как дополнение к MVVM
фреймворку
134
Полезные ссылки
http://reactiveui.net/
https://github.com/reactiveui
http://www.magnuslindhe.com/2015/08/one-way-of-cancelling-commands-using-reactiveui/ - отмена
асинхронной операции, запущенной командой
https://github.com/alexeyzimarev/RxUI6WithAutofac - Rx и Autofac
135
Что дальше делать?
• Посмотреть сэмплы Rx или ReactiveUI
• Попробовать Rx и ReactiveUI на отдельных
формах, если понравится – внедрять:
• ReactiveCommand
• ObservableForProperty
• WhenAny, WhenAnyValue
• Select, Merge, Zip
136
История успеха – GitHub Desktop
137
Спасибо за внимание
Вопросы?
Денис Цветцих
den.tsvettsih@yandex.ru

Как приручить реактивное программирование в XAML приложениях

  • 1.
    Как приручить реактивное программированиев XAML приложениях Денис Цветцих Ice Rock Dev icerockdev.com DotNext Spb 3 июня 2016 dotnext.ru/spb/
  • 2.
    2 Кто я? • 7+лет .NET • Разработка корпоративных приложений • Люблю хипстерские библиотеки 
  • 3.
    3 Почему я здесь? •6 лет назад приручил Rx • 3 года назад приручил ReactiveUI
  • 4.
    4 ReactiveUI – ниодного поста на хабре
  • 5.
    5 Сэмплы не показательны Rx– 101 пример задач, где можно обойтись без Rx ReactiveUI – единственный жизненный сэмпл – поиск (на главной странице)
  • 6.
    6 О чем мыпоговорим? • Что такое реактивное программирование • Примеры задач из продакшен проектов, которые проще решаются с помощью Rx и ReactiveUI • Антипаттерны: как ReactiveUI лучше не использовать
  • 7.
    ЧТО ТАКОЕ РЕАКТИВНОЕПРОГРАММИРОВАНИЕ
  • 8.
    8 Что такое реактивное программирование? Реактивноепрограммирование – это парадигма программирования, ориентированная на потоки данных и распространение изменений. (Википедия)
  • 9.
    9 Парадигма программирования Парадигма программирования– это система идей и понятий, определяющих стиль написания компьютерных программ.
  • 10.
    10 Императивная программа var A= 10; var B = A + 1; // Чему равно В? 11 A = A + 1; // А теперь чему равно В? 11
  • 11.
    11 Реактивная программа var A= 10; var B <- A + 1; <- оператор «судьбы» // Чему равно В? 11 A = A + 1; // А теперь чему равно В? 12
  • 12.
    12 Реактивное программирование Реактивное программирование– это парадигма программирования, ориентированная на потоки данных и распространение изменений. Поток данных – последовательность значений переменной или свойства класса. Распространение изменений – уведомления «заинтересованных» об изменениях.
  • 13.
    13 Pull Pull – классA взаимодействует с классом B и вытягивает из него необходимые данные Class A Class B GetData() Interacts with
  • 14.
    14 Push Push – классB самостоятельно выталкивает данные классу A, как только они становятся доступны. Class A ProcessData() Class BReacts on
  • 15.
  • 16.
    16 Реактивное программирование на.NET • Reactive Extensions (Rx) • Эрик Мейер из MS Research • ReactiveUI • Разработана в MS Research • библиотека на базе Rx для создания элегантных UI для всех XAML платформ
  • 17.
    17 Внутри .NET public interfaceIObservable<T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<T> { void OnNext(T value); void OnCompleted(); void OnError(Exception error); }
  • 18.
    18 Внутри .NET public interfaceIObservable<T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<T> { void OnNext(T value); void OnCompleted(); void OnError(Exception error); }
  • 19.
    19 Внутри .NET public interfaceIObservable<T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<T> { void OnNext(T value); void OnCompleted(); void OnError(Exception error); }
  • 20.
    20 Внутри .NET public interfaceIObservable<T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<T> { void OnNext(T value); void OnCompleted(); void OnError(Exception error); }
  • 21.
    21 Внутри .NET public interfaceIObservable<T> { IDisposable Subscribe(IObserver<T> observer); } public interface IObserver<T> { void OnNext(T value); void OnCompleted(); void OnError(Exception error); }
  • 22.
    22 Оператор судьбы черезRx var A = new Subject<int>(); // B <- A + 1 var B = A.Select(a => a + 1); A.OnNext(10);
  • 23.
    23 Оператор судьбы черезRx var A = new Subject<int>(); // B <- A + 1 var B = A.Select(a => a + 1); A.OnNext(10);
  • 24.
    24 Оператор судьбы черезRx var A = new Subject<int>(); // B <- A + 1 var B = A.Select(a => a + 1); A.OnNext(10);
  • 25.
    25 Оператор судьбы черезRx var A = new Subject<int>(); // B <- A + 1 var B = A.Select(a => a + 1); A.OnNext(10);
  • 26.
  • 27.
  • 28.
  • 29.
    29 Пример Приложение для поискапо материалам конференции DotNext
  • 30.
    30 Для читабельности примеров publicclass Filter : ReactiveObject { private DateTime _fromDate; private DateTime _toDate; public DateTime FromDate { get { return _fromDate; } set { this.RaiseAndSetIfChanged(ref _fromDate, value); } } public DateTime ToDate { get { return _toDate; } set { this.RaiseAndSetIfChanged(ref _toDate, value); } } }
  • 31.
    31 Для читабельности примеров publicclass Filter : ReactiveObject { private DateTime _fromDate; private DateTime _toDate; public DateTime FromDate { get { return _fromDate; } set { this.RaiseAndSetIfChanged(ref _fromDate, value); } } public DateTime ToDate { get { return _toDate; } set { this.RaiseAndSetIfChanged(ref _toDate, value); } } }
  • 32.
    32 Для читабельности примеров publicclass Filter : ReactiveObject { private DateTime _fromDate; private DateTime _toDate; public DateTime FromDate { get { return _fromDate; } set { this.RaiseAndSetIfChanged(ref _fromDate, value); } } public DateTime ToDate { get { return _toDate; } set { this.RaiseAndSetIfChanged(ref _toDate, value); } } }
  • 33.
    33 Для читабельности примеров publicclass Filter { public DateTime FromDate { get; set; } public DateTime ToDate { get; set; } }
  • 34.
    34 Задача 1: подпискана события об изменении свойств фильтра
  • 35.
    35 Обычная подписка насобытие PropertyChanged var filter = new Filter(); // Подписка filter.PropertyChanged += OnPropertyChanged; // Реакция на событие private void OnPropertyChanged (object sender, PropertyChangedEventArgs e) { } // Отписка filter.PropertyChanged -= OnPropertyChanged;
  • 36.
    36 Обычная подписка насобытие PropertyChanged var filter = new Filter(); // Подписка filter.PropertyChanged += OnPropertyChanged; // Реакция на событие private void OnPropertyChanged (object sender, PropertyChangedEventArgs e) { } // Отписка filter.PropertyChanged -= OnPropertyChanged;
  • 37.
    37 var filter =new Filter(); // Подписка filter.PropertyChanged += OnPropertyChanged; // Реакция на событие private void OnPropertyChanged (object sender, PropertyChangedEventArgs e) { } // Отписка filter.PropertyChanged -= OnPropertyChanged; Обычная подписка на событие PropertyChanged
  • 38.
    38 Обычная подписка насобытие PropertyChanged var filter = new Filter(); // Подписка filter.PropertyChanged += OnPropertyChanged; // Реакция на событие private void OnPropertyChanged (object sender, PropertyChangedEventArgs e) { } // Отписка filter.PropertyChanged -= OnPropertyChanged;
  • 39.
    39 filter = newFilter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose(); Rx подписка на событие PropertyChanged
  • 40.
    40 filter = newFilter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose(); Rx подписка на событие PropertyChanged
  • 41.
    41 filter = newFilter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose(); Rx подписка на событие PropertyChanged
  • 42.
    42 filter = newFilter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose(); Rx подписка на событие PropertyChanged
  • 43.
    43 Rx подписка насобытие PropertyChanged filter = new Filter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose();
  • 44.
    44 Rx подписка насобытие PropertyChanged filter = new Filter(); // Подписка IDisposable subscription = Observable.FromEventPattern<PropertyChangedEventArgs> (filter, "PropertyChanged") .Subscribe(OnPropertyChanged); // Реакция на событие private void OnPropertyChanged (EventPattern<PropertyChangedEventArgs> eventPattern) { //eventPattern.Sender и eventPattern.EventArgs } // Отписка subscription.Dispose();
  • 45.
    45 Достоинства Rx подписки •Подписка инкапсулируется в IDisposable объекте, просто сделать отписку • Подписка – это поток данных, над которым можно выполнять операции
  • 46.
    46 Задача 2: подпискана изменение свойства FromDate
  • 47.
    47 Обычная подписка наизменение свойства FromDate Filter filter = new Filter(); filter.PropertyChanged += OnPropertyChanged; private void OnPropertyChanged (object sender, PropertyChangedEventArgs eventArgs) { if (eventArgs.PropertyName == “FromDate") { // Действия } }
  • 48.
    48 Обычная подписка наизменение свойства FromDate Filter filter = new Filter(); filter.PropertyChanged += OnPropertyChanged; private void OnPropertyChanged (object sender, PropertyChangedEventArgs eventArgs) { if (eventArgs.PropertyName == “FromDate") { // Действия } }
  • 49.
    49 Обычная подписка наизменение свойства FromDate Filter filter = new Filter(); filter.PropertyChanged += OnPropertyChanged; private void OnPropertyChanged (object sender, PropertyChangedEventArgs eventArgs) { if (eventArgs.PropertyName == “FromDate") { // Действия } }
  • 50.
    50 ReactiveUI подписка наизменение свойства FromDate var filter = new Filter(); var subscription = filter .ObservableForProperty(f => f.FromDate) .Subscribe(FromDateChanged); private void FromDateChanged (IObservedChange<Filter, DateTime> change) { // Действия }
  • 51.
    51 ReactiveUI подписка наизменение свойства FromDate var filter = new Filter(); var subscription = filter .ObservableForProperty(f => f.FromDate) .Subscribe(FromDateChanged); private void FromDateChanged (IObservedChange<Filter, DateTime> change) { // Действия }
  • 52.
    52 ReactiveUI подписка наизменение свойства FromDate var filter = new Filter(); var subscription = filter .ObservableForProperty(f => f.FromDate) .Subscribe(FromDateChanged); private void FromDateChanged (IObservedChange<Filter, DateTime> change) { // Действия }
  • 53.
    53 ReactiveUI подписка наизменение свойства FromDate var filter = new Filter(); var subscription = filter .ObservableForProperty(f => f.FromDate) .Subscribe(FromDateChanged); private void FromDateChanged (IObservedChange<Filter, DateTime> change) { // Действия }
  • 54.
    54 Достоинства ObservableForProperty • Ненужно проверять, что обрабатываем событие об изменении нужного свойства
  • 55.
    55 Задача 3: действияпри изменении значений свойств
  • 56.
    56 Добавим выбор страныи города Добавим в фильтр: • Список стран и городов • При выборе страны актуализируем список городов
  • 57.
    57 Валидация выбранного периода Условие:FromDate не превосходит ToDate Если FromDate > ToDate, тогда ToDate приравниваем к FromDate Если ToDate < FromDate, тогда FromDate приравниваем к ToDate
  • 58.
    58 Обычное решение private DateTime_fromDate; public DateTime FromDate { get { return _fromDate; } set { _fromDate = value; OnPropertyChanged(); if (_fromDate > ToDate) ToDate = _fromDate; } }
  • 59.
    59 Обычное решение private DateTime_fromDate; public DateTime FromDate { get { return _fromDate; } set { _fromDate = value; OnPropertyChanged(); if (_fromDate > ToDate) ToDate = _fromDate; } }
  • 60.
    60 ObservableForProperty private IDisposable _subscription; publicFilter() { _subscription = this.ObservableForProperty(vm => vm.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); } public DateTime FromDate { get; set; } // PropertyChanged
  • 61.
    61 ObservableForProperty private IDisposable _subscription; publicFilter() { _subscription = this.ObservableForProperty(vm => vm.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); } public DateTime FromDate { get; set; } // PropertyChanged
  • 62.
    62 ObservableForProperty private IDisposable _subscription; publicFilter() { _subscription = this.ObservableForProperty(vm => vm.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); } public DateTime FromDate { get; set; } // PropertyChanged
  • 63.
    63 ObservableForProperty private IDisposable _subscription; publicFilter() { _subscription = this.ObservableForProperty(vm => vm.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); } public DateTime FromDate { get; set; } // PropertyChanged
  • 64.
    64 ObservableForProperty private IDisposable _subscription; publicFilter() { _subscription = this.ObservableForProperty(vm => vm.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); } public DateTime FromDate { get; set; } // PropertyChanged if (_fromDate > ToDate) ToDate = _fromDate;
  • 65.
    65 Зависимости между свойствамив обычном решении public class Filter { public DateTime FromDate { get { return _fromDate; } set { _fromDate = value; if (_fromDate > ToDate) ToDate = FromDate; } } public DateTime ToDate { get { return _toDate; } set { _toDate = value; if (_toDate < FromDate) FromDate = _toDate; } } public List<string> Cities { get { return _cities; } set { _cities = value; SelectedCity = null; } } public string SelectedCountry { get { return _selectedCountry; } set { _selectedCountry = value; Cities = GetCitesForCountry(_selectedCountry); } } }
  • 66.
    66 Зависимости между свойствамив ReactiveUI решении public class Filter { public Filter() { _fromDateSubscription = this.ObservableForProperty(filter => filter.FromDate) .Where(fromDate => fromDate.Value > ToDate) .Subscribe(fromDate => ToDate = fromDate.Value); _toDateSubscription = this.ObservableForProperty(filter => filter.ToDate) .Where(toDate => toDate.Value < FromDate) .Subscribe(toDate => FromDate = toDate.Value); _selectedCountrySubscription = this.ObservableForProperty(filter => filter.SelectedCountry) .Subscribe(country => Cities = GetCitesForCountry(country.Value)); _citiesSubscription = this.ObservableForProperty(filter => Cities) .Subscribe(_ => SelectedCity = null); } public DateTime FromDate { get; set; } public DateTime ToDate { get; set; } public List<string> Countries { get; set; } public List<string> Cities { get; set; } public string SelectedCountry { get; set; } public string SelectedCity { get; set; } }
  • 67.
    67 Достоинства ObservableForProperty • Удалениелогики из сеттеров • Делает код на порядок более читаемым, собирая код зависимости между свойствами в конструкторе
  • 68.
    68 Задача 4: мониторингодновременно нескольких свойств Поля фильтра: • От • До • Страна • Город При изменении любого – обновляем данные
  • 69.
    69 ViewModel с фильтром publicclass ViewModel { public Filter Filter { get; set; } public ViewModel() { Filter = new Filter(); // Подписка на изменение свойств фильтра } }
  • 70.
    70 ViewModel с фильтром publicclass ViewModel { public Filter Filter { get; set; } public ViewModel() { Filter = new Filter(); // Подписка на изменение свойств фильтра } }
  • 71.
    71 ViewModel с фильтром publicclass ViewModel { public Filter Filter { get; set; } public ViewModel() { Filter = new Filter(); // Подписка на изменение свойств фильтра } }
  • 72.
    72 Обычное решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра Filter.PropertyChanged += OnFilterPropertyChanged; } private void OnFilterPropertyChanged (object sender, PropertyChangedEventArgs eventArgs) { if (eventArgs.PropertyName == "FromDate" || eventArgs.PropertyName == "ToDate" || eventArgs.PropertyName == "SelectedCountry" || eventArgs.PropertyName == "SelectedCity") { Refresh(); } }
  • 73.
    73 Обычное решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра Filter.PropertyChanged += OnFilterPropertyChanged; } private void OnFilterPropertyChanged (object sender, PropertyChangedEventArgs eventArgs) { if (eventArgs.PropertyName == "FromDate" || eventArgs.PropertyName == "ToDate" || eventArgs.PropertyName == "SelectedCountry" || eventArgs.PropertyName == "SelectedCity") { Refresh(); } }
  • 74.
    74 ReactiveUI решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра _subscription = Filter.WhenAnyValue( f => f.FromDate, f => f.ToDate, f => f.SelectedCountry, f => f.SelectedCity) .Subscribe(_ => Refresh()); }
  • 75.
    75 ReactiveUI решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра _subscription = Filter.WhenAnyValue( f => f.FromDate, f => f.ToDate, f => f.SelectedCountry, f => f.SelectedCity) .Subscribe(_ => Refresh()); }
  • 76.
    76 ReactiveUI решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра _subscription = Filter.WhenAnyValue( f => f.FromDate, f => f.ToDate, f => f.SelectedCountry, f => f.SelectedCity) .Subscribe(_ => Refresh()); }
  • 77.
    77 ReactiveUI решение public ViewModel() { Filter= new Filter(); // Подписка на изменение свойств фильтра _subscription = Filter.WhenAnyValue( f => f.FromDate, f => f.ToDate, f => f.SelectedCountry, f => f.SelectedCity) .Subscribe(_ => Refresh()); }
  • 78.
    78 Достоинства WhenAnyValue • Подписываемсятолько на нужные нам свойства • Можно подписываться на изменения вложенных свойств • Obj.Deep1.Deep2.Deep3…
  • 79.
    79 Задача 5: проверкаCanExecute команды • Добавляем кнопку «Обновить» на фильтр • Страна и город – обязательные поля
  • 80.
    80 RelayCommand из MvvmLight publicRelayCommand RefreshCommand { get; private set; } public Filter() { RefreshCommand = new RelayCommand( OnRefresh, () => SelectedCity != null && SelectedCountry != null); }
  • 81.
    81 RelayCommand из MvvmLight publicRelayCommand RefreshCommand { get; private set; } public Filter() { RefreshCommand = new RelayCommand( OnRefresh, () => SelectedCity != null && SelectedCountry != null); }
  • 82.
    82 RelayCommand из MvvmLight publicRelayCommand RefreshCommand { get; private set; } public Filter() { RefreshCommand = new RelayCommand( OnRefresh, () => SelectedCity != null && SelectedCountry != null); }
  • 83.
    83 RelayCommand из MvvmLight publicRelayCommand RefreshCommand { get; private set; } public Filter() { RefreshCommand = new RelayCommand( OnRefresh, () => SelectedCity != null && SelectedCountry != null); }
  • 84.
    84 Подергивания  public stringSelectedCountry { get { return _selectedCountry; } set { _selectedCountry = value; OnPropertyChanged(); RefreshCommand.RaiseCanExecuteChanged(); } } public string SelectedCity { get { return _selectedCity; } set { _selectedCity = value; OnPropertyChanged(); RefreshCommand.RaiseCanExecuteChanged(); } }
  • 85.
    85 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; }
  • 86.
    86 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; }
  • 87.
    87 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; }
  • 88.
    88 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; }
  • 89.
    89 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; }
  • 90.
    90 ReactiveCommand public Filter() { var canExecute= this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.Create(canExecute); command.Subscribe(_ => OnRefresh()); RefreshCommand = command; } public string SelectedCountry { get; set; } // RefreshCommand.RaiseCanExecuteChanged();
  • 91.
    91 Достоинства ReactiveCommand • Ненужны «подергивания» в сеттерах • При изменения условия нельзя забыть убрать или добавить «подергивание»
  • 92.
    92 Задача 6: командазапускает асинхронное действие • При выполнении команды – вызов вебсервиса • Пока не получим ответ от сервиса – команду нельзя выполнить
  • 93.
    93 Асинхронное действие RelayCommand publicbool IsBusy { get; set; } //RefreshCommand.RaiseCanExecuteChanged(); public Filter(Task refreshTask) { RefreshTask = refreshTask; RefreshCommand = new RelayCommand(OnRefresh, () => SelectedCity != null && SelectedCountry != null && !IsBusy); }
  • 94.
    94 Асинхронное действие RelayCommand publicbool IsBusy { get; set; } //RefreshCommand.RaiseCanExecuteChanged(); public Filter(Task refreshTask) { RefreshTask = refreshTask; RefreshCommand = new RelayCommand(OnRefresh, () => SelectedCity != null && SelectedCountry != null && !IsBusy); }
  • 95.
    95 Асинхронное действие RelayCommand publicbool IsBusy { get; set; } //RefreshCommand.RaiseCanExecuteChanged(); public Filter(Task refreshTask) { RefreshTask = refreshTask; RefreshCommand = new RelayCommand(OnRefresh, () => SelectedCity != null && SelectedCountry != null && !IsBusy); }
  • 96.
    96 Асинхронное обновление private asyncvoid OnRefresh() { IsBusy = true; await RefreshTask; // запрос на сервер IsBusy = false; }
  • 97.
    97 Асинхронное обновление private asyncvoid OnRefresh() { IsBusy = true; await RefreshTask; // запрос на сервер IsBusy = false; }
  • 98.
    98 Асинхронное обновление private asyncvoid OnRefresh() { IsBusy = true; await RefreshTask; // запрос на сервер IsBusy = false; }
  • 99.
    99 ReactiveAsyncCommand public Filter(Func<object, Task>refreshFunc) { var canExecute = this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.CreateAsyncTask( canExecute, refreshFunc); RefreshCommand = command; }
  • 100.
    100 ReactiveAsyncCommand public Filter(Func<object, Task>refreshFunc) { var canExecute = this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.CreateAsyncTask( canExecute, refreshFunc); RefreshCommand = command; }
  • 101.
    101 ReactiveAsyncCommand public Filter(Func<object, Task>refreshFunc) { var canExecute = this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.CreateAsyncTask( canExecute, refreshFunc); RefreshCommand = command; }
  • 102.
    102 ReactiveAsyncCommand public Filter(Func<object, Task>refreshFunc) { var canExecute = this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.CreateAsyncTask( canExecute, refreshFunc); RefreshCommand = command; }
  • 103.
    103 ReactiveAsyncCommand public Filter(Func<object, Task>refreshFunc) { var canExecute = this.WhenAny( f => f.SelectedCity, f => f.SelectedCountry, (city, country) => city.Value != null && country.Value != null); var command = ReactiveCommand.CreateAsyncTask( canExecute, refreshFunc); RefreshCommand = command; }
  • 104.
  • 105.
    105 Достоинства ReactiveAsyncCommand • Ненужно отдельного свойства для мониторинга начала/конца операции
  • 106.
    106 Задача 7: поискпо подстроке Вместо фильтра делаем поиск Если длина строки < 3, то ждем 0,5 сек перед поиском Если длина строки >= 3, то ждем 0,25 сек перед поиском
  • 107.
    107 Поиск по строке publicclass ViewModel { public Filter Filter { get; set; } public string SearchText { get; set; } }
  • 108.
    108 Поиск по строке publicclass ViewModel { public Filter Filter { get; set; } public string SearchText { get; set; } }
  • 109.
    109 Поиск по строке publicclass ViewModel { public Filter Filter { get; set; } public string SearchText { get; set; } }
  • 110.
    110 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 111.
    111 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 112.
    112 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 113.
    113 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 114.
    114 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 115.
    115 Where, Throttle var textObservable= this.ObservableForProperty(s => s.SearchText); var firstRule = textObservable .Where(text => text.Value == null || text.Value.Length < 3) .Throttle(TimeSpan.FromMilliseconds(500)); var secondRule = textObservable .Where(text => text.Value != null && text.Value.Length >= 3) .Throttle(TimeSpan.FromMilliseconds(250));
  • 116.
    116 Merge, ObserveOnDispatcher var resultObservable= firstRule.Merge(secondRule); resultObservable .ObserveOnDispatcher() .Subscribe(Refresh);
  • 117.
    117 Merge, ObserveOnDispatcher var resultObservable= firstRule.Merge(secondRule); resultObservable .ObserveOnDispatcher() .Subscribe(Refresh);
  • 118.
    118 Merge, ObserveOnDispatcher var resultObservable= firstRule.Merge(secondRule); resultObservable .ObserveOnDispatcher() .Subscribe(Refresh);
  • 119.
    119 Достоинства • На порядокменьшее кода, чем написали бы для обычного решения • Код простой и читаемый
  • 120.
  • 121.
    121 View – ViewModelмаппинг private void RegisterParts(IMutableDependencyResolver dependencyResolver) { dependencyResolver.RegisterConstant(this, typeof(IScreen)); dependencyResolver.Register( () => new WelcomeView(), typeof(IViewFor<WelcomeViewModel>)); }
  • 122.
    122 View – ViewModelмаппинг private void RegisterParts(IMutableDependencyResolver dependencyResolver) { dependencyResolver.RegisterConstant(this, typeof(IScreen)); dependencyResolver.Register( () => new WelcomeView(), typeof(IViewFor<WelcomeViewModel>)); }
  • 123.
    123 ViewModel First навигация varviewModel = (ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel)); viewModel.Title = "New ViewModel"; Router.Navigate.Execute(viewModel);
  • 124.
    124 ViewModel First навигация varviewModel = (ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel)); viewModel.Title = "New ViewModel"; Router.Navigate.Execute(viewModel);
  • 125.
    125 ViewModel First навигация varviewModel = (ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel)); viewModel.Title = "New ViewModel"; Router.Navigate.Execute(viewModel);
  • 126.
    126 ViewModel First навигация varviewModel = (ViewModel) Locator.CurrentMutable.GetService(typeof(ViewModel)); viewModel.Title = "New ViewModel"; Router.Navigate.Execute(viewModel);
  • 127.
    127 Нет CompositeUI <Grid UseLayoutRounding="True"> <!-- COOLSTUFF:RoutedViewHost RoutedViewHost will take a Router and display whatever ViewModel is at the front of the route stack. --> <rx:RoutedViewHost Router="{Binding Router}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" /> </Grid>
  • 128.
    128 Нельзя избавиться отSplat IoC В Splat • Получить экземпляр сервиса – аналог ServiceLocator Locator.CurrentMutable.GetService(typeof(Service)) Выход • Реализовать IMutableDependencyResolver (для AutoFac)
  • 129.
    129 Недостатки ReactiveUI какMVVM • Нельзя задать конвенцию именования для маппинга View- ViewModel • Нет API, упрощающего задание соответствия View-ViewModel • Splat – это ServiceLocator в чистом виде • Нельзя заменить Splat на свой любимый IoC • Нельзя реализовать CompositeUI • ViewModelFirst навигация неудобная для UWP, WinRT, Windows Phone
  • 130.
  • 131.
    131 Достоинства ReactiveUI • Меньшеинфраструктурного кода • Повышает читаемость кода, группируя зависимости между свойствами в конструкторе • Решение типовых задач • Подписка на изменение свойства • Подписка на изменение нескольких свойств • Автоматическая проверка состояния команды при: • Изменении любого свойства, входящего в условие • Начале/окончании асинхронной операции, запускаемой командой
  • 132.
    132 Главное достоинство Упрощается решениетиповых задач разработчика XAML приложений, а не какой-то экзотики!
  • 133.
    133 Недостатки Rx иReactiveUI • Требует аккуратного использования, написать что-то нечитаемое просто • ReactiveUI лучше использовать как дополнение к MVVM фреймворку
  • 134.
    134 Полезные ссылки http://reactiveui.net/ https://github.com/reactiveui http://www.magnuslindhe.com/2015/08/one-way-of-cancelling-commands-using-reactiveui/ -отмена асинхронной операции, запущенной командой https://github.com/alexeyzimarev/RxUI6WithAutofac - Rx и Autofac
  • 135.
    135 Что дальше делать? •Посмотреть сэмплы Rx или ReactiveUI • Попробовать Rx и ReactiveUI на отдельных формах, если понравится – внедрять: • ReactiveCommand • ObservableForProperty • WhenAny, WhenAnyValue • Select, Merge, Zip
  • 136.
  • 137.

Editor's Notes

  • #4 Случайно начали использовать Думали что все другие тоже используют Оказалось, что нет
  • #6 их почти нет, они слабые, искусственные и показывают далеко не все возможности(Codefest приложение). Е
  • #7 Цель – показать реальные задачи, решаемые через Rx и ReactiveUI
  • #15 Push уведомления для телефонов – пример реактивного программирования
  • #31 чтобы не перегружать примеры, считаем что классы реализуют INPC а свойства бросают события об изменении
  • #32 чтобы не перегружать примеры, считаем что классы реализуют INPC а свойства бросают события об изменении
  • #33 чтобы не перегружать примеры, считаем что классы реализуют INPC а свойства бросают события об изменении
  • #34 чтобы не перегружать примеры, считаем что классы реализуют INPC а свойства бросают события об изменении
  • #40 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #41 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #42 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #43 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #44 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #45 Чем этот метод лучше – создается Disposable объект подписки Говорить ли о том, как он устроен?
  • #59 Для ToDate - аналогично
  • #60 Для ToDate - аналогично
  • #68 Это достоинство проявляется только на больших решениях
  • #81 То же для DelegateCommand из Prism
  • #82 То же для DelegateCommand из Prism
  • #83 То же для DelegateCommand из Prism
  • #84 То же для DelegateCommand из Prism
  • #93 Команда неактивна и по условию, и во время действия
  • #104 Демо
  • #106 Отмена – добавить в Демо
  • #118 Рассказать зачем нужен ObserveOnDispatcher. Может быть не UI поток из-за таймера
  • #122 Нет отдельного класса для маппинга, API не самое удобное
  • #123 Нет отдельного класса для маппинга, API не самое удобное
  • #124 Неудобно для UWP, WinRT (Противоречит API, можно потерять стек при восстановлении приложения) Нет специальной фабрики ViewModel
  • #125 Неудобно для UWP, WinRT (Противоречит API, можно потерять стек при восстановлении приложения) Нет специальной фабрики ViewModel
  • #126 Неудобно для UWP, WinRT (Противоречит API, можно потерять стек при восстановлении приложения) Нет специальной фабрики ViewModel
  • #127 Неудобно для UWP, WinRT (Противоречит API, можно потерять стек при восстановлении приложения) Нет специальной фабрики ViewModel
  • #128 Отображается только последняя показанная ViewModel, сделать CompositeUI в принципе нельзя.