Что нужно знать для чтения этого руководства?
Для того чтобы понимать написанное здесь, Вы должны знать: базовые сведения о программировании и ООП, синтаксис C# (включая события, методы расширения, лямбда-выражения), LINQ, интерфейсы: INotifyPropertyChanged, INotifyCollectionChanged, IDisposable.Желательно знать отличия делегатов от деревьев выражений.
Для того чтобы представить себе какие преимущества можно получить при использовании ObservableComputations, Вы должны знать: о привязке данных (binding) в WPF (или в другой UI платформе: Xamarin, Blazor), особенно её связь с интерфейсами INotifyPropertyChanged и INotifyCollectionChanged, свойство DbSet.Local (local data) из Entity framework, асинхронные запросы Entity framewok.
Это кросс-платформенная .NET библиотека для вычислений, аргументами и результатами которых являются объекты реализующие интерфейсы INotifyPropertyChanged и INotifyCollectionChanged (ObservableCollection). Вычисления включают в себя те же вычисления которые есть в LINQ, вычисление произвольного выражения и некоторые дополнительные возможности. Вычисления реализованы как методы расширения, подобно LINQ методам. Вы можете комбинировать вызовы методов расширения ObservavleComputations (цепочка вызовов и вложенные вызовы), как Вы это делаете в LINQ. Поддерживаются вычисления в фоновых потоках, в том числе параллельные, а также обработка событий CollectionChanged и PropertyChanged связанная со временем.
ObservableComputations это простая в использовании и мощная реализация парадигмы реактивного программирования. С ObservableComputations, Ваш код будет более соответствовать функциональному (декларативному) стилю, чем при использовании стандартного LINQ. Реактивное программирование в функциональном стиле делает Ваш код понятнее, короче, надёжнее и производительнее. С реактивным программирование Вы можете быстрее создавать богатый пользовательский интерфейс. Смотрите подробнее в разделе Области применения и преимущества.
Ближайшими аналогами ObservableComputations являются следующие библиотеки: Obtics, OLinq, NFM.Expressions, BindableLinq, ContinuousLinq.
Подробности
ObservableComputations не является аналогом Reactive Extensions. Вот главные отличия ObservableComputations от Reactive Extensions:- Reactive Extensions абстрагирован от конкретного события и от семантики событий: это библиотека для обработки всех возможных событий. Reactive Extensions обрабатывает все события одинаковым образом, а вся специфика только в пользовательском коде. ObservableComputations сфокусирован только на двух событиях: CollectionChanged и PropertyChanged и приносит большую пользу обрабатывая их
- Библиотека Reactive Extensions предоставляет поток событий. ObservableComputations предоставляет не только поток событий изменения данных, но вычисленные в данный момент данные
Часть задач, которые Вы решали с помощью Reactive Extensions, теперь проще и эффективней решить с помощью ObservableComputations. Вы можете использовать ObservableComputations отдельно или вместе с Reactive Extensions. ObservableComputations не заменит Reactive Extensions:
- при обработке событий связанной со временем (Throttle, Buffer). ObservableComputation позволяет реализовать связанную со временем обработку событий CollectionChanged и PropertyChanged путем взаимодействия с Reactive Extensions (смотрите пример здесь)
- при обработке событий не связанных с данными (например, нажатие клавиш), особенно при необходимости комбинировать эти события
- при работе с асинхронными операциями (метод Observable.FromAsyncPattern)
Подробности
Библиотека ReactiveUI (и её подбиблиотека DynamicData) не абстрагированы от интерфейсов INotifyPropertyChanged и INotifyCollectionChanged и при работе с этими интерфейсами позволяет делать примерно тоже самое что и ObservableComputations, но ObservableComputations менее многословна, проще в использовании, более декларативна, меньше дергает исходные данные. Почему?-
Реактивность ObservableComputations основана только на двух событиях: CollectionChanged и PropertyChanged. Такая реактивность является "родной" для ObservableComputations. Реактивность ReactiveUI основана на интерфейсах унаследованных от Reactive Extensions: IObserver<T>, IObservable<T>, а также дополнительных интерфейсах для работы с коллекциями (содержащиеся в DynamicData): IChangeSet и IChangeSet<TObject>. ReactiveUI осуществляет двунаправленное преобразование между этими интерфейсами и интерфейсами INotifyPropertyChanged и INotifyCollectionChanged. Даже с учётом этого преобразования интерфейсы INotifyPropertyChanged и INotifyCollectionChanged выглядят чужеродными для ReactiveUI
-
ObservableComputations не требует уникальности коллекций-источников и наличия в них свойства Id. Вместо этого ObservableComputations учитывает порядок элементов коллекции источника в вычисленной коллекции.
-
ObservableComputations больше похожа на обычный LINQ
-
Интерфейсы INotifyPropertyChanged и INotifyCollectionChanged тесно интегрированы в UI платформы от Microsoft (WPF, Xamarin, Blazor).
-
ReactiveUI это MVVM framework с реактивной функциональностью. ObservableComputations нацелен только на реактивную функциональность. С ObservableComputations Вы можете использовать любой MVVM framework, реализовать паттерн MVVM самостоятельно или вообще не следовать паттерну MVVM
Вы можете сравнить эти библиотеки и ObservableComputations в действии, см.
Реализованы все функции и операторы, необходимые для разработки реальных приложений. Весь критичный код покрыт юнит-тестами. Я работаю над увеличением процента покрытия.
Все релизы ObservableComputations доступны на NuGet. Там же можно посмотреть историю изменений в разделе Release Notes.
- написать в чат
- создать GitHub issue
- создать GitHub discussion
- связаться со мной по электронной почте
- Нужны презентации, посты в блогах, руководства и отзывы
- Приветствуются комментарии и замечания к документации
- Сообщите о баге
- Предложите новую функцию
- Создайте демо-проект
- Создайте xml документацию в коде
- Создайте юнит-тест
- Нужна красивая иконка
Изучив данные примеры, Вы сможете начать использовать ObservableComputations. Остальную часть данного руководства можно читать по мере необходимости.
Аналоги LINQ методов
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Price = 15},
new Order{Num = 2, Price = 15},
new Order{Num = 3, Price = 25},
new Order{Num = 4, Price = 27},
new Order{Num = 5, Price = 30},
new Order{Num = 6, Price = 75},
new Order{Num = 7, Price = 80}
});
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
Filtering<Order> expensiveOrders =
orders
.Filtering(o => o.Price > 25)
.For(consumer);
Debug.Assert(expensiveOrders is ObservableCollection<Order>);
checkFiltering(orders, expensiveOrders); // Prints "True"
expensiveOrders.CollectionChanged += (sender, eventArgs) =>
{
// see the changes (add, remove, replace, move, reset) here
checkFiltering(orders, expensiveOrders); // Prints "True"
};
// Start the changing...
orders.Add(new Order{Num = 8, Price = 30});
orders.Add(new Order{Num = 9, Price = 10});
orders[0].Price = 60;
orders[4].Price = 10;
orders.Move(5, 1);
orders[1] = new Order{Num = 10, Price = 17};
checkFiltering(orders, expensiveOrders); // Prints "True"
Console.ReadLine();
consumer.Dispose();
}
static void checkFiltering(
ObservableCollection<Order> orders,
Filtering<Order> expensiveOrders)
{
Console.WriteLine(expensiveOrders.SequenceEqual(
orders.Where(o => o.Price > 25)));
}
}
}
Как Вы видите метод расширения Filtering это аналог метода Where из LINQ. Метод расширения Filtering возвращает экземпляр класса Filtering<Order>. Класс Filtering<TSourceItem> реализует интерфейс INotifyCollectionChanged и наследуется от ObservableCollection<TSourceItem>. Изучая код выше Вы увидите, что expensiveOrders не перевычисляется заново каждый раз когда коллекция orders меняется или меняется свойство Price какого-либо заказа, в коллекции expensiveOrders происходят только те изменения, которые отражают отдельное изменение в коллекции orders или отдельное изменение свойства Price какого-либо заказа. Согласно терминологии реактивного программирования, такое поведение определяет модель распространения изменений, как "push".
В коде выше, во время выполнения метода расширения For, происходит подписка на следующие события: событие CollectionChanged коллекции orders и событие PropertyChanged каждого экземпляра класса Order. Во время выполнения метода consumer.Dispose() происходит отписка от событий.
Сложность выражения предиката переданного в метод расширения Filtering (o => o.Price > 25) не ограничена. Выражение может включать в себя результаты вызовов методов ObservavleComputations, включая аналоги LINQ.
using System;
using System.ComponentModel;
using System.Diagnostics;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(Price)));
}
}
private byte _discount;
public byte Discount
{
get => _discount;
set
{
_discount = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(Discount)));
}
}
}
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Order order = new Order{Num = 1, Price = 100, Discount = 10};
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
Computing<decimal> discountedPriceComputing =
new Computing<decimal>(
() => order.Price - order.Price * order.Discount / 100)
.For(consumer);
Debug.Assert(discountedPriceComputing is INotifyPropertyChanged);
printDiscountedPrice(discountedPriceComputing);
discountedPriceComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<decimal>.Value))
{
// see the changes here
printDiscountedPrice(discountedPriceComputing);
}
};
// Start the changing...
order.Price = 200;
order.Discount = 15;
Console.ReadLine();
consumer.Dispose();
}
static void printDiscountedPrice(Computing<decimal> discountedPriceComputing)
{
Console.WriteLine($"Discounted price is ₽{discountedPriceComputing.Value}");
}
}
}
В этом примере кода мы следим за значением выражения цены со скидкой. Класс Computing<TResult> реализует интерфейс INotifyPropertyChanged. Сложность отслеживаемого выражения не ограничена. Выражение может включать в себя результаты вызовов методов ObservavleComputations, включая аналоги LINQ.
Так же как в предыдущем примере во время выполнения метода расширения For происходит подписка на событие PropertyChanged экземпляра класса Order. Во время выполнения метода consumer.Dispose() происходит отписка от событий.
Если Вы хотите чтобы выражение () => order.Price - order.Price * order.Discount / 100 было чистой функцией, нет проблем:
Expression<Func<Order, decimal>> discountedPriceExpression =
o => o.Price - o.Price * o.Discount / 100;
// We start using ObservableComputations here!
Computing<decimal> discountedPriceComputing =
order.Using(discountedPriceExpression).For(consumer);
Теперь выражение discountedPriceExpression может быть использовано для других экземпляров класса Order.
WPF, Xamarin, Blazor. Вы можете привязывать (binding) элементы пользовательского интерфейса (controls) к экземплярам классов ObservableComputations (Filtering, Computing etc.). Если Вы так делаете, Вам не нужно беспокоиться о том, что Вы забыли вызвать событие PropertyChanged для вычисляемых свойств или вручную обработать изменение в какой-либо коллекции. С ObservableComputations Вы определяете как значение должно вычисляться (декларативный стиль), всё остальное ObservableComputations сделает за Вас.
Такой подход облегчает асинхронное программирование. Вы можете показать пользователю форму и начать загружать исходные данные (из БД или web-сервиса) в фоне. По мере того как исходные данные загружаются, форма наполняется вычисленными данными. Пользователь увидит форму быстрее (пока исходные данные загружаются в фоне, Вы можете начать рендеринг). Если форма уже показана пользователю, Вы можете обновить исходные данные в фоне, вычисляемые данные отображенные на форме обновятся благодаря ObservableComputations. Так же ObservableComputations включают специальные средства для многопоточных вычислений. Подробности см. здесь.
Если у Вас есть сложные вычисления, часто меняющиеся исходные данные и\или данных много, вы можете получить выигрыш в производительности с ObservableComputations, так как Вам не надо перевычислять данные с нуля каждый раз когда меняются исходные данные. Каждое маленькое изменение в исходных данных вызывает маленькое изменение в данных вычисленных средствами ObservableComputations. Производительность пользовательского интерфейса возрастает, так как необходимость в ререндеренге уменьшается (только изменённые данные рендерятся) и данные из внешних источников (DB, web-сервис) загружаются в фоне (см. предыдущий раздел).
- Меньше шаблонного императивного кода. Больше чистого декларативного (в функциональном стиле) кода. Общий объём кода уменьшается.
- Меньшая вероятность ошибки программиста: вычисляемые данные показанные пользователю пользователю будут всегда соответствовать пользовательскому вводу и данным загруженным из внешних источников (DB, web-сервис).
- Код загрузки исходных данные и код для вычисления данных отображаемых в пользовательском интерфейсе могут быть чётко разделены.
- Вы можете не беспокоиться о том, что забыли обновить вычисляемые данные. Все вычисляемые данные будут обновляться автоматически.
ObservableComputations облегчают создание дружелюбного пользовательского интерфейса.
- Пользователю не нужно вручную обновлять вычисляемые данные.
- Пользователь видит вычисляемые данные всегда, а не только по запросу.
- Вам не нужно обновлять вычисляемые данные по таймеру.
- Не нужно блокировать пользовательский интерфейс во время вычисления и отображения большого объема данных (показывая при этом индикатор загрузки). Данные могут обновляться небольшими порциями, при этом пользователь может продолжать работать.
Перед изучение таблицы, представленной ниже, пожалуйста, обратите внимание на то, что
-
CollectionComputing<TSourceItem> наследуется от ObservableCollection<TSourceItem>. Этот класс реализует интерфейс INotifyCollectionChanged.
-
ScalarComputing<TValue> реализует интерфейс IReadScalar<TValue>;
public interface IReadScalar<out TValue> : System.ComponentModel.INotifyPropertyChanged
{
TValue Value { get;}
}
Свойство Value позволяет получить текущий результат вычисления. Из кода выше вы можете увидеть, что ScalarComputation<TValue> позволяет следить за значением свойства Value с помощью события PropertyChanged интерфейса INotifyPropertyChanged.
Аналоги MS LINQ | |||
Группа перегруженных методов ObservableComputations |
Группа перегруженных методов MS LINQ |
Возвращаемые объект является |
Примечание |
Appending | Append | CollectionComputing | |
Aggregating | Aggregate | ScalarComputing | |
AllComputing | All | ScalarComputing | |
AnyComputing | Any | ScalarComputing | |
Averaging | Average | ScalarComputing | |
Casting | Cast | CollectionComputing | |
Concatenating | Concat | CollectionComputing | Элементами коллекции-источника могут быть INotifyCollectionChanged или IReadScalar<INotifyCollectionChanged> |
ContainsComputing | Contains | ScalarComputing | |
ObservableCollection .Count property |
Count | ||
Not implemented | DefaultIfEmpty | ||
Distincting | Distinct | CollectionComputing | |
ItemComputing | ElementAtOrDefault | ScalarComputing | Если запрошенный индекс выходит за границы коллекции-источника свойство ScalarComputing<TSourceItem>.Value возвращает значение по умолчанию |
Excepting | Except | CollectionComputing | |
FirstComputing | FirstOrDefault | ScalarComputing | Если размер коллекции-источника нулевой свойство ScalarComputing<TSourceItem>.Value возвращает значение по умолчанию |
Grouping | Group | CollectionComputing | Может содержать группу с ключём null |
GroupJoining | GroupJoin | CollectionComputing | |
PredicateGroupJoining | CollectionComputing | ||
IndicesComputing | IndexOf | CollectionComputing | |
Intersecting | Intersect | CollectionComputing | |
Joining | Join | CollectionComputing | |
LastComputing | LastOrDefault | ScalarComputing | Если размер коллекции-источника нулевой свойство ScalarComputing<TSourceItem>.Value возвращает значение по умолчанию |
Maximazing | Max | ScalarComputing | Если размер коллекции-источника нулевой свойство ScalarComputing<TSourceItem>.Value возвращает значение по умолчанию |
Minimazing | Min | ScalarComputing | Если размер коллекции-источника нулевой свойство ScalarComputing<TSourceItem>.Value возвращает значение по умолчанию |
OfTypeComputing | OfType | CollectionComputing | |
Ordering | Order | CollectionComputing | |
Ordering | OrderByDescending | CollectionComputing | |
Prepending | Prepend | CollectionComputing | |
SequenceComputing | Range | CollectionComputing | |
Reversing | Reverse | CollectionComputing | |
Selecting | Select | CollectionComputing | |
SelectingMany | SelectMany | CollectionComputing | |
Skiping | Skip | CollectionComputing | |
SkipingWhile | SkipWhile | CollectionComputing | |
StringsConcatenating | string.Join | ScalarComputing | |
Summarizing | Sum | ScalarComputing | |
Taking | Take | CollectionComputing | |
TakingWhile | TakeWhile | CollectionComputing | |
ThenOrdering | ThenBy | CollectionComputing | |
ThenOrdering | ThenByDescending | CollectionComputing | |
Dictionaring | ToDictionary | Dictionary | |
HashSetting | ToHashSet | HashSet | |
Uniting | Union | CollectionComputing | |
Filtering | Where | CollectionComputing | |
Zipping | Zip | CollectionComputing | |
Другие функции | |||
Группа перегруженных методов ObservableComputations |
Возвращаемые объект является | Примечание | |
Binding class | см. больше здесь | ||
CollectionDispatching | CollectionComputing | см. больше здесь | |
CollectionDisposing | CollectionComputing | см. больше здесь | |
CollectionPausing | CollectionComputing | см. больше здесь | |
CollectionItemProcessing CollectionItemsProcessing |
CollectionComputing | см. больше здесь | |
Computing | ScalarComputing | см. больше здесь | |
Differing | ScalarComputing | см. больше здесь | |
NullPropagating | ScalarComputing | Аналог оператора «?.». Эта реализация необходима из-за CS8072 |
|
Paging | CollectionComputing | содержит подмножество элементов коллекции соответствующее странице с определённым номером и размером |
|
PreviousTracking | ScalarComputing | см. больше здесь | |
PropertyAccessing | ScalarComputing | см. больше здесь | |
PropertyDispatching | ScalarComputing | см. больше здесь | |
ScalarDispatching | ScalarComputing | см. больше здесь | |
ScalarDisposing | ScalarComputing | см. больше здесь | |
ScalarPausing | ScalarComputing | см. больше здесь | |
ScalarProcessing | ScalarComputing | см. больше здесь | |
Using | ScalarComputing | см. больше здесь и здесь | |
WeakPreviousTracking | ScalarComputing | см. больше здесь |
Для всех вычислений имеющих параметры типа INotifyCollectionChanged: null значение параметра обрабатывается как пустая коллекция.
Для всех вычислений имеющих параметры типа IReadScalar<INotifyCollectionChanged>: null значение свойства IReadScalar<INotifyCollectionChanged>.Value обрабатывается как пустая коллекция.
Для того чтобы вычисление обрабатывало изменения в своих источниках, оно должно быть подписано на события PropertyChanged и CollectionChanged своих источников. В этом случае вычисление находится в активном состояние (IsActive == true). При подписке на событие возникает ссылка от источника события (источника вычисления) к делегату обработчика события. Сам делегат в свою очередь ссылается на объект в контексте которого он выполняется (вычисление). Таким образом в активном состоянии источники вычисления ссылаются на вычисление. Вычисление также ссылается на источники. Это означается, при сборке мусора в активном состоянии вычисление может выгрузиться из памяти только вместе со своими источниками. Иными словами в активном состоянии вычислении может выгрузиться из памяти только если нет ссылок ни на вычисление, ни на источники. Иногда возникает ситуации, когда источники нужны (на них есть ссылки), а вычисление уже не нужно и его необходимо выгрузить из памяти. Это возможно только, если вычисление отпишется от событий PropertyChanged и CollectionChanged своих источников. В этом случае вычисление находится в неактивном состоянии. В неактивном состоянии вычисления-коллекции пусты, а вычисления-скаляры имеют значение по умолчанию.
ObservableComputations имеет API для управления активностью вычислений. Основная идея этого состоит в том, что когда вычисление кому-то нужно, оно активно. Если вычисление никому не нужно, оно становится неактивным. Объектами, которые могут наждаться в вычислениях, являются экземпляры класса OcConsumer:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Price = 15},
new Order{Num = 2, Price = 15},
new Order{Num = 3, Price = 25},
new Order{Num = 4, Price = 27},
new Order{Num = 5, Price = 30},
new Order{Num = 6, Price = 75},
new Order{Num = 7, Price = 80}
});
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
Selecting<Order, decimal> highPrices =
orders
.Filtering(o => o.Price > 25)
.Selecting(o => o.Price);
// Computations is not active
Debug.Assert(!highPrices.IsActive);
Debug.Assert(!((Filtering<Order>)highPrices.Source).IsActive);
check(orders, highPrices); // Prints "False"
// Now we make computations active
highPrices.For(consumer); // Selecting and Filtering computations is needed for consumer
// Computations is active
Debug.Assert(highPrices.IsActive);
Debug.Assert(((Filtering<Order>)highPrices.Source).IsActive);
check(orders, highPrices); // Prints "True"
Debug.Assert(highPrices is ObservableCollection<decimal>);
check(orders, highPrices); // Prints "True"
highPrices.CollectionChanged += (sender, eventArgs) =>
{
// see the changes (add, remove, replace, move, reset) here
check(orders, highPrices); // Prints "True"
};
// Start the changing...
orders.Add(new Order{Num = 8, Price = 30});
orders.Add(new Order{Num = 9, Price = 10});
orders[0].Price = 60;
orders[4].Price = 10;
orders.Move(5, 1);
orders[1] = new Order{Num = 10, Price = 17};
check(orders, highPrices); // Prints "True"
consumer.Dispose(); // the consumer no longer needs its computations
check(orders, highPrices); // Prints "False"
// Computations is not active
Debug.Assert(!highPrices.IsActive);
Debug.Assert(!((Filtering<Order>)highPrices.Source).IsActive);
Console.ReadLine();
}
static void check(
ObservableCollection<Order> orders,
Selecting<Order, decimal> expensiveOrders)
{
Console.WriteLine(expensiveOrders.SequenceEqual(
orders.Where(o => o.Price > 25).Select(o => o.Price)));
}
}
}
Обратите внимание на вызов метода расширения For. Этот метод расширения можно вызвать для всех экземпляров вычислений. Если источником вычисления является другое вычисление, оно также становится необходимым для потребителя.
Класс OcConsumer реализует интерфейс IDisposable. При вызове consumer.Dispose() consumer отказывается от всех своих вычислений. Один экземпляр OcConsumer может нуждаться в нескольких вычисления. Вычисление может быть необходимым для нескольких экземпляров OcConsumer. Только когда от вычисления откажутся все экземпляры OcConsumer оно становится неактивным. Сказанное выше можно проиллюстрировать диаграммой состояний:
Аргументы методов расширения ObservableComputations могут быть переданы двумя путями: как обозреваемые и как не обозреваемые.
using System;
using System.Collections.ObjectModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Person
{
public string Name { get; set; }
}
public class LoginManager
{
public Person LoggedInPerson { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person[] allPersons =
new []
{
new Person(){Name = "Vasiliy"},
new Person(){Name = "Nikolay"},
new Person(){Name = "Igor"},
new Person(){Name = "Aleksandr"},
new Person(){Name = "Ivan"}
};
ObservableCollection<Person> hockeyTeam =
new ObservableCollection<Person>(new []
{
allPersons[0],
allPersons[2],
allPersons[3]
});
LoginManager loginManager = new LoginManager();
loginManager.LoggedInPerson = allPersons[0];
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeam.ContainsComputing(loginManager.LoggedInPerson)
.For(consumer);
isLoggedInPersonHockeyPlayer.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(ContainsComputing<Person>.Value))
{
// see the changes here
}
};
// Start the changing...
hockeyTeam.RemoveAt(0); // 🙂
hockeyTeam.Add(allPersons[0]); // 🙂
loginManager.LoggedInPerson = allPersons[4]; // 🙁!
Console.ReadLine();
consumer.Dispose();
}
}
}
В приведенном коде мы вычисляем является ли залогиненный пользователь хоккейным игроком. Выражение "loginManager.LoggedInPerson" переданное в метод ContainsComputing вычисляется (оценивается) алгоритмами ObservableComputations только один раз: когда класс ContainsComputing<Person> инстанцируется (когда вызывается метод расшерения ContainsComputing). Если свойство LoggedInPerson меняется, это изменение не отражается в isLoggedInPersonHockeyPlayer.
Конечно, Вы можете использовать более сложное выражение, чем "loginManager.LoggedInPerson для передачи как аргумента в любой метод расширения ObservableComputations. Как видите передача аргумента типа T как не обозреваемого это обычная передача аргумента типа T.
В предыдущем примере, мы предполагали, что наше приложение не поддерживает выход пользователя (logout) (и последующий вход (login)). Другими словами приложение не обрабатывает изменения свойства LoginManager.LoggedInPerson. Давайте добавим функциональность logout в наше приложение:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.System.Linq.Expressions;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Person
{
public string Name { get; set; }
}
public class LoginManager : INotifyPropertyChanged
{
private Person _loggedInPerson;
public Person LoggedInPerson
{
get => _loggedInPerson;
set
{
_loggedInPerson = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(LoggedInPerson)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Person[] allPersons =
new []
{
new Person(){Name = "Vasiliy"},
new Person(){Name = "Nikolay"},
new Person(){Name = "Igor"},
new Person(){Name = "Aleksandr"},
new Person(){Name = "Ivan"}
};
ObservableCollection<Person> hockeyTeam =
new ObservableCollection<Person>(new []
{
allPersons[0],
allPersons[2],
allPersons[3]
});
LoginManager loginManager = new LoginManager();
loginManager.LoggedInPerson = allPersons[0];
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeam.ContainsComputing<Person>(new Computing(
() => loginManager.LoggedInPerson))
.For(consumer);
isLoggedInPersonHockeyPlayer.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(ContainsComputing<Person>.Value))
{
// see the changes here
}
};
// Start the changing...
hockeyTeam.RemoveAt(0); // 🙂
hockeyTeam.Add(allPersons[0]); // 🙂
loginManager.LoggedInPerson = allPersons[4]; // 🙂!!!
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде выше мы передаём аргумент в метод расширения ContainsComputing как IReadScalar<Person> (а не как Person как в предыдущем разделе). Computing<Person> реализует IReadScalar<Person>. IReadScalar<TValue> первоначально был упомянут в разделе "Полный список операторов". Как видите, если Вы хотите передать аргумент типа T как обозреваемый, Вы должны выполнить обычную передачу аргумента типа IReadScalar<T>. В этом случае используется другая перегруженная вервия метода ContainsComputing, в отличии от версии, которая использовалась в предыдущем разделе. Это даёт нам возможность следить за изменениями свойства LoginManager.LoggedInPerson. Теперь изменения свойства LoginManager.LoggedInPerson отражаются в isLoggedInPersonHockeyPlayer. Обратите внимание на то, что теперь класс LoginManager реализует INotifyPropertyChanged.
Код выше может быть укорочен:
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeam.ContainsComputing(() => loginManager.LoggedInPerson);
При использовании этой перегруженной версии метода ContainsComputing, переменные loggedInPersonExpression и isLoggedInPersonHockeyPlayer больше не нужны. Эта перегруженная версии метода ContainsComputing создаёт Computing<Person> "за ценой".
Другой укороченный вариант:
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeam.ContainsComputing<Person>(
Expr.Is(() => loginManager.LoggedInPerson).Computing());
Первоначальный вариант может быть полезен, если Вы хотите переиспользовать new Computing(() => loginManager.LoggedInPerson) для других вычислений помимо isLoggedInPersonHockeyPlayer. Первый укороченный вариант не позволяет этого. Укороченные варианты могут быть полезны для expression-bodied properties and methods.
Конечно, вы можете использовать более сложное выражение чем "() => loginManager.LoggedInPerson" для передачи в качестве аргумента в любой метод расширения ObservableComputations.
Как Вы видите все вызовы LINQ подобных методов расширения ObservableComputations в общем виде могут быть представлены как
sourceCollection.ExtensionMethodName(arg1, arg2, ...);
sourceCollection это первый аргумент в объявлении метода расширения. Поэтому подобно другим аргументам он тоже может быть передан как не обозреваемый и как обозреваемый. До сих пор мы передавали коллекцию источник как не обозреваемый аргумент (это было простое выражение состоящее из одной переменной, конечно вы можете использовать более сложные выражения, суть остаётся та же). Теперь давайте попробуем передать коллекцию источник как обозреваемый аргумент:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq.Expressions;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Person
{
public string Name { get; set; }
}
public class LoginManager : INotifyPropertyChanged
{
private Person _loggedInPerson;
public Person LoggedInPerson
{
get => _loggedInPerson;
set
{
_loggedInPerson = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(LoggedInPerson)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class HockeyTeamManager : INotifyPropertyChanged
{
private ObservableCollection<Person> _hockeyTeamInterested;
public ObservableCollection<Person> HockeyTeamInterested
{
get => _hockeyTeamInterested;
set
{
_hockeyTeamInterested = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(HockeyTeamInterested)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Person[] allPersons =
new []
{
new Person(){Name = "Vasiliy"},
new Person(){Name = "Nikolay"},
new Person(){Name = "Igor"},
new Person(){Name = "Aleksandr"},
new Person(){Name = "Ivan"}
};
ObservableCollection<Person> hockeyTeam1 =
new ObservableCollection<Person>(new []
{
allPersons[0],
allPersons[2],
allPersons[3]
});
ObservableCollection<Person> hockeyTeam2 =
new ObservableCollection<Person>(new []
{
allPersons[1],
allPersons[4]
});
LoginManager loginManager = new LoginManager();
loginManager.LoggedInPerson = allPersons[0];
HockeyTeamManager hockeyTeamManager = new HockeyTeamManager();
Expression<Func<ObservableCollection<Person>>> hockeyTeamInterestedExpression =
() => hockeyTeamManager.HockeyTeamInterested;
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
Computing<ObservableCollection<Person>> hockeyTeamInterestedComputing =
hockeyTeamInterestedExpression.Computing();
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeamInterestedComputing.ContainsComputing(
() => loginManager.LoggedInPerson)
.For(consumer);
isLoggedInPersonHockeyPlayer.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(ContainsComputing<Person>.Value))
{
// see the changes here
}
};
// Start the changing...
hockeyTeamManager.HockeyTeamInterested = hockeyTeam1;
hockeyTeamManager.HockeyTeamInterested.RemoveAt(0);
hockeyTeamManager.HockeyTeamInterested.Add(allPersons[0]);
loginManager.LoggedInPerson = allPersons[4];
loginManager.LoggedInPerson = allPersons[2];
hockeyTeamManager.HockeyTeamInterested = hockeyTeam2;
hockeyTeamManager.HockeyTeamInterested.Add(allPersons[2]);
Console.ReadLine();
consumer.Dispose();
}
}
}
Как и в предыдущем разделе код выше может быть укорочен:
Expression<Func<ObservableCollection<Person>>> hockeyTeamInterestedExpression =
() => hockeyTeamManager.HockeyTeamInterested;
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
hockeyTeamInterestedExpression
.ContainsComputing(() => loginManager.LoggedInPerson)
.For(consumer);
или:
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
Expr.Is(() => hockeyTeamManager.HockeyTeamInterested)
.ContainsComputing(() => loginManager.LoggedInPerson)
.For(consumer);
или:
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
Expr.Is(() => hockeyTeamManager.HockeyTeamInterested).Computing()
.ContainsComputing(
() => loginManager.LoggedInPerson)
.For(consumer);
или:
ContainsComputing<Person> isLoggedInPersonHockeyPlayer =
Expr.Is(() => hockeyTeamManager.HockeyTeamInterested).Computing()
.ContainsComputing(
() => loginManager.LoggedInPerson);
Конечно, Вы можете использовать более сложное выражение чем "() => hockeyTeamManager.HockeyTeamInterested для передачи в качестве аргумента в любой метод расширения ObservableComputations.
Мы продолжаем рассматривать пример из предыдущего раздела. Мы использовали следующий код для того чтобы отследить изменения в hockeyTeamManager.HockeyTeamInterested:
new Computing<ObservableCollection<Person>>(
() => hockeyTeamManager.HockeyTeamInterested)
Может показаться на первый взгляд, что следующий код будет работать и isLoggedInPersonHockeyPlayer будет отражать изменения в hockeyTeamManager.HockeyTeamInterested:
Computing<bool> isLoggedInPersonHockeyPlayer = new Computing<bool>(() =>
hockeyTeamManager.HockeyTeamInterested.ContainsComputing(
() => loginManager.LoggedInPerson).Value);
В этом коде "hockeyTeamManager.HockeyTeamInterested" передан в метод расширения ContainsComputing как не обозреваемый, и не имеет значения, что "hockeyTeamManager.HockeyTeamInterested" это часть выражения переданного в конструктор класса Computing<bool>, изменения в "hockeyTeamManager.HockeyTeamInterested" не будут отражаться в isLoggedInPersonHockeyPlayer. Правило обозреваемых и не обозреваемых аргументов применяется только в одном направлении: от вложенных (обёрнутых) к внешним (оборачивающим) вызовам. Другими словами, правило обозреваемых и не обозреваемых аргументов всегда справедливо, независимо от того является ли вычисление корневым или вложенным.
Вот другой пример:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private string _type;
public string Type
{
get => _type;
set
{
_type = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Type)));
}
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Type = "VIP"},
new Order{Num = 2, Type = "Regular"},
new Order{Num = 3, Type = "VIP"},
new Order{Num = 4, Type = "VIP"},
new Order{Num = 5, Type = "NotSpecified"},
new Order{Num = 6, Type = "Regular"},
new Order{Num = 7, Type = "Regular"}
});
ObservableCollection<string> selectedOrderTypes = new ObservableCollection<string>(new []
{
"VIP", "NotSpecified"
});
OcConsumer consumer = new OcConsumer();
ObservableCollection<Order> filteredByTypeOrders =
orders.Filtering(o =>
selectedOrderTypes.ContainsComputing(() => o.Type).Value)
.For(consumer);
filteredByTypeOrders.CollectionChanged += (sender, eventArgs) =>
{
// see the changes (add, remove, replace, move, reset) here
};
// Start the changing...
orders.Add(new Order{Num = 8, Type = "VIP"});
orders.Add(new Order{Num = 9, Type = "NotSpecified"});
orders[4].Type = "Regular";
orders.Move(4, 1);
orders[0] = new Order{Num = 10, Type = "Regular"};
selectedOrderTypes.Remove("NotSpecified");
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде выше мы создаём вычисление "filteredByTypeOrders" которое отражает изменения в коллекция orders и selectedOrderTypes и в свойстве Order.Type. Обратите внимание на аргумент переданный в ContainsComputing. Следующий код не будет отражать изменения в свойстве Order.Type:
ObservableCollection<Order> filteredByTypeOrders = orders.Filtering(o =>
selectedOrderTypes.ContainsComputing(o.Type).Value);
Единственный способ изменить результат вычисления это изменить исходные данные. Вот пример кода:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public int Num {get; set;}
private string _manager;
public string Manager
{
get => _manager;
set
{
_manager = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Manager)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Manager = "Stepan"},
new Order{Num = 2, Manager = "Aleksey"},
new Order{Num = 3, Manager = "Aleksey"},
new Order{Num = 4, Manager = "Oleg"},
new Order{Num = 5, Manager = "Stepan"},
new Order{Num = 6, Manager = "Oleg"},
new Order{Num = 7, Manager = "Aleksey"}
});
OcConsumer consumer = new OcConsumer();
Filtering<Order> stepansOrders =
orders.Filtering(o =>
o.Manager == "Stepan")
.For(consumer);
stepansOrders.InsertItemRequestHandler = (i, order) =>
{
orders.Add(order);
order.Manager = "Stepan";
};
Order newOrder = new Order(){Num = 8};
stepansOrders.Add(newOrder);
Debug.Assert(stepansOrders.Contains(newOrder));
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде выше мы создаём вычисление stepansOrders (заказы Степана). Мы устанавливаем делегат, в качестве значения свойства stepansOrders.InsertItemRequestHandler для того чтобы определить как изменить коллекцию orders и order, который нужно добавить, так чтобы он был включён в вычисление stepansOrders.
Обратите внимание на то, что метод Add это член интерфейса ICollection<T> interface.
Данная возможность может быть полезна если Вы передаёте stepansOrders в код, который абстрагирован от того, чем является stepansOrders: вычислением или обычной коллекцией. Этот код знает только то, что stepansOrders реализует ICollection<T> interface и иногда хочет добавлять заказы в stepansOrders. Таким кодом, например, может двунаправленная привязка данных в WPF или привязка к свойству ItemsSource у DataGrid.
Свойства аналогичные InsertItemRequestHandler существуют и для других операций (remove, set, move, clear). Все свойства имеют постфикс "RequestHandler".
Иногда возникает необходимость производить какие-либо действия
- с добавляемыми в коллекцию элементами
- с удаляемыми из коллекции элементами
- элементами перемещаемыми внутри коллекции
Конечно вы можете обработать все текущие элементs коллекции, затем подписаться на событие CollectionChanged, но библиотека ObservableComputations содержит более простое и эффективное средство.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Client : INotifyPropertyChanged
{
public string Name { get; set; }
private bool _online;
public bool Online
{
get => _online;
set
{
_online = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Online)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class NetworkChannel : IDisposable
{
public NetworkChannel(string clientName)
{
ClientName = clientName;
Console.WriteLine($"NetworkChannel to {ClientName} has been created");
}
public string ClientName { get; set; }
public void Dispose()
{
Console.WriteLine($"NetworkChannel to {ClientName} has been disposed");
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Client> clients = new ObservableCollection<Client>(new Client[]
{
new Client(){Name = "Sergey", Online = false},
new Client(){Name = "Evgeney", Online = true},
new Client(){Name = "Anatoley", Online = false},
new Client(){Name = "Timofey", Online = true}
});
OcConsumer consumer = new OcConsumer();
Filtering<Client> onlineClients = clients.Filtering(c => c.Online);
onlineClients.CollectionItemProcessing(
(newClient, collectionProcessing) =>
new NetworkChannel(newClient.Name),
(oldClient, collectionProcessing, networkChannel) =>
networkChannel.Dispose())
.For(consumer);
clients[2].Online = true;
clients.RemoveAt(1);
consumer.Dispose();
Console.ReadLine();
}
}
}
Делегат переданный в параметр newItemProcessor вызывается
- при активации экземпляра класса CollectionProcessing<TSourceItem, TReturnValue> (если коллекция-источник (onlineClients) содержит элементы в момент активации),
- при добавление элемента в коллекцию-источник,
- при замене элемента в коллекции-источнике,
- при reset коллекции источника и она содержит элементы после reset,
- в случае если коллекция-источник передана как скаляр (IReadScalar<TValue>), и у него меняется значение свойства Value на коллекцию, которая содержит элементы.
Делегат переданный в параметр oldItemProcessor вызывается
- при деактивации экземпляра класса CollectionProcessing<TSourceItem, TReturnValue>,
- при удалении элемента из коллекции-источника,
- при замене элемента в коллекции-источнике (установка элемента коллекции по индексу),
- при reset коллекции источника (метод Clear()).
- в случае если коллекция-источник передана как скаляр (IReadScalar<TValue>), и у него меняется значение свойства Value.
Есть также возможность передать делегат moveItemProcessor для обработки события перемещения элемента в коллекции-источнике.
Метод CollectionItemProcessing обрабатывает элементы коллекции по одному. Метод CollectionItemsProcessing позволяет за один раз обработать множество элементов коллекции. Обработка множества элементов происходит при активации, деактивации и при Reset (Clear) коллекции-источника. Метод CollectionItemsProcessing не удобен для обработки изменений связанных с единственным элементом коллекции источника.
Существует также перегруженная версия метода CollectionItemProcessing (CollectionItemsProcessing), которая принимает делегат newItemProcessor (newItemsProcessor), возвращающий пустое значение (void).
IReadScalar<TValue> упоминается в первый раз здесь. Вы можете обрабатывать изменение значения свойства Value, подписавшись на событие PropertyChanged, но по аналогии с обработкой изменений в ObservableCollection<T> ObservableComputations позволяет обрабатывать изменения в IReadScalar<TValue> проще и эффективнее:
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Client : INotifyPropertyChanged
{
private NetworkChannel _networkChannel;
public NetworkChannel NetworkChannel
{
get => _networkChannel;
set
{
_networkChannel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NetworkChannel)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class NetworkChannel : IDisposable
{
public NetworkChannel(int num)
{
Num = num;
}
public int Num { get; set; }
public void Open()
{
Console.WriteLine($"NetworkChannel #{Num} has been opened");
}
public void Dispose()
{
Console.WriteLine($"NetworkChannel #{Num} has been disposed");
}
}
class Program
{
static void Main(string[] args)
{
var networkChannel = new NetworkChannel(1);
Client client = new Client() {NetworkChannel = networkChannel};
OcConsumer consumer = new OcConsumer();
Computing<NetworkChannel> networkChannelComputing
= new Computing<NetworkChannel>(() => client.NetworkChannel);
networkChannelComputing.ScalarProcessing(
(newNetworkChannel, scalarProcessing) =>
newNetworkChannel.Open(),
(oldNetworkChannel, scalarProcessing) =>
oldNetworkChannel.Dispose())
.For(consumer);
client.NetworkChannel = new NetworkChannel(2);
client.NetworkChannel = new NetworkChannel(3);
consumer.Dispose();
Console.ReadLine();
}
}
}
Существует также перегруженная версия метода ScalarProcessing, которая принимает делегат newValueProcessor, возвращающий не пустое значение.
Если элементы коллекции реализуют IDisposable Вам может понадобиться вызвать метод Dispose для всех элементов покидающих коллекцию (Remove, Replace, Clear). Вы можете использовать CollectionProcessing чтобы достичь этого, как в предыдущем разделе. Другой вариант использовать метод CollectionDisposing:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Client : INotifyPropertyChanged
{
public string Name { get; set; }
private bool _online;
public bool Online
{
get => _online;
set
{
_online = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Online)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class NetworkChannel : IDisposable
{
public NetworkChannel(string clientName)
{
ClientName = clientName;
Console.WriteLine($"NetworkChannel to {ClientName} has been created");
}
public string ClientName { get; set; }
public void Dispose()
{
Console.WriteLine($"NetworkChannel to {ClientName} has been disposed");
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Client> clients = new ObservableCollection<Client>(new Client[]
{
new Client(){Name = "Sergey", Online = false},
new Client(){Name = "Evgeney", Online = true},
new Client(){Name = "Anatoley", Online = false},
new Client(){Name = "Timofey", Online = true}
});
OcConsumer consumer = new OcConsumer();
Filtering<Client> onlineClients = clients.Filtering(c => c.Online);
onlineClients
.CollectionItemProcessing(
(newClient, collectionProcessing) =>
new NetworkChannel(newClient.Name))
.CollectionDisposing()
.For(consumer);
clients[2].Online = true;
clients.RemoveAt(1);
consumer.Dispose();
Console.ReadLine();
}
}
}
Метод ScalarDisposing позволяет вызвать метод Dispose для старых значений IReadScalar:
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Client : INotifyPropertyChanged
{
private NetworkChannel _networkChannel;
public NetworkChannel NetworkChannel
{
get => _networkChannel;
set
{
_networkChannel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NetworkChannel)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class NetworkChannel : IDisposable
{
public NetworkChannel(int num)
{
Num = num;
}
public int Num { get; set; }
public void Open()
{
Console.WriteLine($"NetworkChannel #{Num} has been opened");
}
public void Dispose()
{
Console.WriteLine($"NetworkChannel #{Num} has been disposed");
}
}
class Program
{
static void Main(string[] args)
{
var networkChannel = new NetworkChannel(1);
Client client = new Client() {NetworkChannel = networkChannel};
Computing<NetworkChannel> networkChannelComputing
= new Computing<NetworkChannel>(() => client.NetworkChannel);
OcConsumer consumer = new OcConsumer();
networkChannelComputing.ScalarProcessing(
(newNetworkChannel, scalarProcessing) =>
newNetworkChannel.Open())
.ScalarDisposing()
.For(consumer);
client.NetworkChannel = new NetworkChannel(2);
client.NetworkChannel = new NetworkChannel(3);
consumer.Dispose();
Console.ReadLine();
}
}
}
Когда выполняется обработчик события PropetyChanged или CollectionChanged вычисления, это вычисление обрабатывает некоторое изменение источника и находится в несогласованном состоянии (IsConsistent == false). Все изменения источников, внесенные в это время (накладывающиеся изменения), будут отложены до тех пор, пока вычисление не завершит обработку исходного изменения источника.
Рассмотрим следующий код:
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public enum RelationType { Parent, Child }
public struct Relation
{
public string From {get; set;}
public string To {get; set;}
public RelationType Type {get; set;}
public Relation CorrespondingRelation =>
new Relation(){
From = this.To,
To = this.From,
Type = this.Type == RelationType.Child
? RelationType.Parent
: RelationType.Child};
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Relation> relations =
new ObservableCollection<Relation>(new []
{
new Relation{From = "Valentin", To = "Filipp", Type = RelationType.Child},
new Relation{From = "Filipp", To = "Valentin", Type = RelationType.Parent},
new Relation{From = "Olga", To = "Evgeny", Type = RelationType.Child},
new Relation{From = "Evgeny", To = "Olga", Type = RelationType.Parent}
});
OcConsumer consumer = new OcConsumer();
Ordering<Relation, string> orderedRelations =
relations.Ordering(r => r.From)
.For(consumer);
orderedRelations.CollectionChanged += (sender, eventArgs) =>
{
switch (eventArgs.Action)
{
case NotifyCollectionChangedAction.Add:
Relation newRelation = (Relation) eventArgs.NewItems[0];
if (relations.Contains(newRelation.CorrespondingRelation))
return;
relations.Add(newRelation.CorrespondingRelation); // this change
// was not reflected in orderedRelations for now
// (it's processing was deferred and will be done latter)
// so following assertion is passes
Debug.Assert(!orderedRelations.Contains(newRelation.CorrespondingRelation));
// It's because orderedRelations is processing change "relations.Add(relation);" now and cannot process other changes
// State of orderedRelations is inconsistent:
Debug.Assert(!orderedRelations.IsConsistent);
break;
case NotifyCollectionChangedAction.Remove:
//...
break;
}
};
Relation relation = new Relation{From = "Arseny", To = "Dmitry", Type = RelationType.Parent};
relations.Add(relation);
// at this point orderedRelations has completed processing of change "relations.Add(relation);".
// All deferred changes have been processed also
// so following assertion is passes
Debug.Assert(orderedRelations.Contains(relation.CorrespondingRelation));
// State of orderedRelations is consistent:
Debug.Assert(orderedRelations.IsConsistent);
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде у нас есть коллекция отношений: relations. Коллекция имеет избыточность: если коллекция имеет отношение A к B типа "Родитель", она должна содержать соответствующее отношение: B к A типа "Ребёнок", и наоборот. Также мы имеем вычисляемую упорядоченную коллекцию отношений orderedRelations. Наша задача поддержать целостность коллекции отношений: если кто-то меняет мы должны отреагировать и восстановить целостность. Представьте, что единственным способом сделать это является подписка на событие CollectionChanged коллекции orderedRelations (по каким-то причинам мы не можем подписаться на событие CollectionChanged коллекции relations). В коде выше мы предполагаем только один тип изменений: Add.
К пользовательскому коды относятся:
-
Селекторы это выражения, которые предаются в качестве аргумента в следующие методы расширения: Selecting, SelectingMany, Grouping, GroupJoining, Dictionaring, Hashing, Ordering, ThenOrdering, PredicateGroupJoining
-
Предикаты это выражения, которые предаются в качестве аргумента в метод расширения Filtering.
-
Функции агрегирования это делегаты, которые передаются в метод расширения Aggregating
-
Произвольные выражения это выражения, которые предаются в качестве аргумента в методы расширения Computing и Using.
-
Обработчики запросов на изменение результатов вычислений описаны в разделе здесь.
-
Код вызванный с помощью методов OcDispatcher.Invoke*.
Вот код иллюстрирующий отладку произвольного выражения (другие типы могут быть отлажены аналогичным образом):
using System;
using System.ComponentModel;
using System.Threading;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class ValueProvider : INotifyPropertyChanged
{
private int _value;
public int Value
{
get => _value;
set
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
OcConfiguration.SaveInstantiationStackTrace = true;
OcConfiguration.TrackComputingsExecutingUserCode = true;
ValueProvider valueProvider = new ValueProvider(){Value = 2};
OcConsumer consumer = new OcConsumer();
Computing<decimal> computing1 =
new Computing<decimal>(() => 1 / valueProvider.Value)
.For(consumer);
Computing<decimal> computing2 =
new Computing<decimal>(() => 1 / (valueProvider.Value - 1))
.For(consumer);;
try
{
valueProvider.Value = new Random().Next(0, 1);
}
catch (DivideByZeroException exception)
{
Console.WriteLine($"Exception stacktrace:\n{exception.StackTrace}");
IComputing computing = StaticInfo.ComputingsExecutingUserCode[Thread.CurrentThread.ManagedThreadId];
Console.WriteLine($"\nComputing which caused the exception has been instantiated by the following stacktrace :\n{computing.InstantiationStackTrace}");
Console.WriteLine($"\nSender of event now processing is :\n{computing.HandledEventSender.ToStringSafe()}");
Console.WriteLine($"\nArgs for the event that is currently being processed is :\n{computing.HandledEventArgs.ToStringAlt()}");
}
Console.ReadLine();
consumer.Dispose();
}
}
}
Как Вы видите exception.StackTrace указывает на строку, которая вызвала исключение: valueProvider.Value = new Random().Next(0, 1);. Эта строка не указывает на вычисление, которое вызвало исключение: computing1 or computing2. Чтобы определить исключение, которое вызвало исключение мы должны взглянуть на свойство StaticInfo.ComputingsExecutingUserCode[Thread.CurrentThread.ManagedThreadId].InstantiatingStackTrace. Это свойство содержит трассировку стека инстанцирования вычисления.
По умолчанию ObservableComputations не сохраняет трассировки стека инстанцирования вычислений по соображениям производительности. Чтобы сохранять эти трассировки стека используйте свойство OcConfiguration.SaveInstantiationStackTrace.
По умолчанию ObservableComputations не следит за вычислениями выполняющими пользовательский код по соображениям производительности. Для того чтобы следить за вычислениями выполняющими пользовательский код используйте свойство OcConfiguration.TrackComputingsExecutingUserCode. Если пользовательский код был вызван из пользовательского кода другого вычисления, то StaticInfo.ComputingsExecutingUserCode[Thread.CurrentThread.ManagedThreadId].UserCodeIsCalledFrom будет указывать на это вычисление.
Все необработанные исключения выброшенные в пользовательском коде фатальны, так как внутреннее состояние вычисление становится повреждённым. Обратите внимание на проверки на null.
Работа с вычислениями в фоновых потоках описана здесь.
using System;
using System.ComponentModel;
using System.Threading;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class ValueProvider : IReadScalar<int>
{
private int _value;
public int Value
{
get => _value;
set
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
OcConfiguration.SaveInstantiationStackTrace = true;
OcConfiguration.TrackComputingsExecutingUserCode = true;
OcConfiguration.SaveOcDispatcherInvocationInstantiationStackTrace = true;
OcConfiguration.SaveOcDispatcherInvocationExecutionStackTrace = true;
ValueProvider valueProvider = new ValueProvider(){Value = 2};
OcDispatcher ocDispatcher = new OcDispatcher();
System.AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
Thread.CurrentThread.IsBackground = true;
Invocation currentInvocation = StaticInfo.OcDispatchers[ocDispatcher.ManagedThreadId].CurrentInvocation;
Console.WriteLine($"Exception stacktrace:\n{currentInvocation.InstantiationStackTrace}");
Console.WriteLine($"\nComputing which caused the exception has been instantiated by the following stacktrace :\n{StaticInfo.ComputingsExecutingUserCode[Thread.CurrentThread.ManagedThreadId].InstantiationStackTrace}");
Console.WriteLine($"\nDispatch computing which caused the exception has been instantiated by the following stacktrace :\n{((IComputing)currentInvocation.Context).InstantiationStackTrace}");
while (true)
Thread.Sleep(TimeSpan.FromHours(1));
};
OcConsumer consumer = new OcConsumer();
ScalarDispatching<int> valueProviderDispatching =
valueProvider.ScalarDispatching(ocDispatcher)
.For(consumer);
ocDispatcher.Pass();
Computing<decimal> computing1 =
new Computing<decimal>(() => 1 / valueProviderDispatching.Value)
.For(consumer);
Computing<decimal> computing2 =
new Computing<decimal>(() => 1 / (valueProviderDispatching.Value - 1))
.For(consumer);
valueProvider.Value = new Random().Next(0, 2);
Console.ReadLine();
consumer.Dispose();
}
}
}
Данный пример аналогичен предыдущему, за исключением
- Свойств, которые содержат информацию об исключении
- Установки параметров конфигурации Configuration.SaveOcDispatcherInvocationInstantiationStackTrace и Configuration.TrackOcDispatcherInvocations
Свойства OcConfiguration.SaveOcDispatcherInvocationExecutionStackTrace, Invocation.ExecutionStackTrace, Invocation.Executor и Invocation.Parent могут пригодиться, если вы вызывали методы OcDispatcher.ExecuteOtherInvocations или OcDispatcher.Invoke* находясь в потоке OcDispatcher.
Дополнительные события для обработки изменений: PreCollectionChanged, PreValueChanged, PostCollectionChanged, PostValueChanged
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private double _price;
public double Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
private bool _discount;
public bool Discount
{
get => _discount;
set
{
_discount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Discount)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Order order = new Order(){Price = 100};
OcConsumer consumer = new OcConsumer();
Computing<string> messageForUser = null;
Computing<double> priceDiscounted =
new Computing<double>(() => order.Discount
? order.Price - order.Price * 0.1
: order.Price)
.For(consumer);
priceDiscounted.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<double>.Value))
Console.WriteLine(messageForUser.Value);
};
messageForUser =
new Computing<string>(() => order.Price > priceDiscounted.Value
? $"Your order price is ₽{order.Price}. You have a discount! Therefore your price is ₽{priceDiscounted.Value}!"
: $"Your order price is ₽{order.Price}")
.For(consumer);
order.Discount = true;
Console.ReadLine();
consumer.Dispose();
}
}
}
Код sdit имеет следующий вывод:
Your order price is ₽100
Хотя мы могли ожидать:
Your order price is ₽100. You have a discount! Therefore your price is ₽90!
Почему? Мы подписались на событие priceDiscounted.PropertyChanged перед тем как messageForUser сделал это. Обработчики событий вызываются в порядке подписки (это деталь реализации .NET). Поэтому мы считываем messageForUser.Value перед тем как messageForUser обрабатывает изменение order.Discount.
Вот исправленный код:
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private double _price;
public double Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
private bool _discount;
public bool Discount
{
get => _discount;
set
{
_discount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Discount)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Order order = new Order(){Price = 100};
OcConsumer consumer = new OcConsumer();
Computing<string> messageForUser = null;
Computing<double> priceDiscounted =
new Computing<double>(() => order.Discount
? order.Price - order.Price * 0.1
: order.Price)
.For(consumer);
// HERE IS THE FIX!
priceDiscounted.PostValueChanged += (sender, eventArgs) =>
{
Console.WriteLine(messageForUser.Value);
};
messageForUser =
new Computing<string>(() => order.Price > priceDiscounted.Value
? $"Your order price is ₽{order.Price}. You have a discount! Therefore your price is ₽{priceDiscounted.Value}!"
: $"Your order price is ₽{order.Price}")
.For(consumer);
order.Discount = true;
Console.ReadLine();
consumer.Dispose();
}
}
}
Вместо priceDiscounted.PropertyChanged мы подписываемся на priceDiscounted.PostValueChanged. Это событие возникает после PropertyChanged, поэтому мы можем быть уверены: все зависимые вычисления обновили свои значения. PostValueChanged объявлено в ScalarComputing<TValue>. Computing<string> наследует ScalarComputing<TValue>. ScalarComputing<TValue> впервые упомянут здесь. ScalarComputing<TValue> содержит событие PreValueChanged. Это событие позволяет посмотреть состояние вычислений до изменения. Если вы хотите обрабатывать событие изменения свойства Вашего объекта (не вычисления как в предыдущем примере) и обработчик читает зависимые вычисления (подобно предыдущему примеру) Вы должны определить своё событие и вызывать его.
CollectionComputing<TItem> содержит события PreCollectionChanged и PostCollectionChanged. CollectionComputing<TItem> впервые упомянут здесь). Если Вы хотите обрабатывать событие изменения Вашей коллекции, реализующей INotifyCollectionChanged (не вычисляемой коллекции, (например, ObservableCollection<TItem>) и обработчик читает зависимые вычисления Вы можете использовать ObservableCollectionExtended<TItem> вместо Вашей коллекции. Этот класс наследует ObservableCollection<TItem> и содержит события PreCollectionChanged и PostCollectionChanged . Также Вы можете использовать метод расширения Extending. Этот метод создаёт ObservableCollectionExtended<TItem> из INotifyCollectionChanged.
CollectionComputing<TSourceItem>) и ScalarComputing<TSourceItem>)
- поддерживают несколько читающих потоков одновременно, если во время чтения не изменяются пишущим потоком. Исключение: вычисление ConcurrentDictionaring, которое поддерживает одновременно несколько читающих потоков и один пишущий.
- не поддерживают одновременные изменения несколькими пишущими потоками.
Вычисления изменяются пишущим потоком когда
- обрабатывают события CollectionChanged и PropertyChanged объектов-источников
- выполняется активация или инактивация
Код окна WPF приложения:
<Window
x:Class="ObservableComputationsExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ObservableComputationsExample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="uc_this"
Title="ObservableComputationsExample"
Width="800"
Height="450"
mc:Ignorable="d"
Closed="mainWindow_OnClosed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label
x:Name="uc_LoadingIndicator"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Left">
Loading source data...
</Label>
<Label
Grid.Row="1"
Grid.Column="0"
FontWeight="Bold">
Unpaid orders
</Label>
<ListBox
Grid.Row="2"
Grid.Column="0"
DisplayMemberPath="Num"
ItemsSource="{Binding UnpaidOrders, ElementName=uc_this}" />
<Label
Grid.Row="1"
Grid.Column="1"
FontWeight="Bold">
Paid orders
</Label>
<ListBox
Grid.Row="2"
Grid.Column="1"
DisplayMemberPath="Num"
ItemsSource="{Binding PaidOrders, ElementName=uc_this}" />
</Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _ocConsumer = new OcConsumer();
public MainWindow()
{
Orders = new ObservableCollection<Order>();
fillOrdersFromDb();
PaidOrders = Orders.Filtering(o => o.Paid).For(_ocConsumer);
UnpaidOrders = Orders.Filtering(o => !o.Paid).For(_ocConsumer);
InitializeComponent();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
this.Dispatcher.Invoke(() => Orders.Add(order), DispatcherPriority.Background);
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocConsumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num)
{
Num = num;
}
public int Num { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
В этом примере мы показываем пользователю форму не дожидаясь пока закончится загрузка данных из БД. Пока идёт загрузка, форма рендерится и пользователь знакомится её содержимым. Заметьте, что код загрузки исходных данных абстрагирован от вычислений над ними (PaidOrders и UnpaidOrders).
В предыдущем примере в фоновом потоке выполнялась только загрузка данных из БД. Сами вычисления (PaidOrders и UnpaidOrders) выполнялись в главном потоке (поток пользовательского интерфейса). Иногда необходимо выполнять вычисление в фоновом потоке, а в главном потоке получать только конечные результаты вычисления:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
// OcDispatcher for computations in the background thread
ObservableComputations.OcDispatcher _ocDispatcher = new ObservableComputations.OcDispatcher();
public MainWindow()
{
Orders = new ObservableCollection<Order>();
WpfOcDispatcher wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
fillOrdersFromDb();
PaidOrders =
Orders.CollectionDispatching(_ocDispatcher) // direct the computation to the background thread
.Filtering(o => o.Paid)
.CollectionDispatching(wpfOcDispatcher, _ocDispatcher, (int)DispatcherPriority.Background) // return the computation to the main thread from the background one
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.Paid)
.For(_consumer);
InitializeComponent();
}
private void fillOrdersFromDb()
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 10000; i++)
{
Order order = new Order(i);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
this.Dispatcher.Invoke(() => Orders.Add(order), DispatcherPriority.Background);
}
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocDispatcher.Dispose();
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num)
{
Num = num;
}
public int Num { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.BeginInvoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
В этом примере мы грузим данные из БД в главном потоке, но фильтрация коллекции-источника Orders для получения оплаченных заказов (PaidOrders) производится в фоновом потоке.
Класс ObservableComputations.OcDispatcher очень похож на класс System.Windows.Threading.Dispatcher. Класс ObservableComputations.OcDispatcher ассоциирован с единственным потоком. В этом потоке вы можете выполнять делегаты, вызывая методы ObservableComputations.OcDispatcher.Invoke*.
Метод CollectionDispatching перенаправляет все изменения коллекции источника в поток целевого диспетчера (параметр distinationDispatcher).
В момент вызова метода CollectionDispatching происходит перечисление коллекции-источника (Orders или Orders.CollectionDispatching(_ocDispatcher).Filtering(o => o.Paid)) и подписка на её событие CollectionChanged. При этом коллекция-источник не должна меняться. При вызове .CollectionDispatching(_ocDispatcher), коллекция Orders не меняется. При вызове CollectionDispatching(wpfOcDispatcher, _ocDispatcher) коллекция Orders.CollectionDispatching(_ocDispatcher).Filtering(o => o.Paid) может меняться в потоке _ocOcDispatcher, но так как мы передаём _ocDispatcher в параметр sourceOcDispatcher, то перечисление коллекции-источника и подписка на её событие CollectionChanged происходит в потоке _ocDispatcher, что гарантирует отсутствие изменений коллекции-источника при перечислении. Так как при вызове .CollectionDispatching(_ocDispatcher), коллекция Orders не меняется, то передавать wpfOcDispatcher в параметр sourceOcDispatcher смысла нет, тем более что в момент вызова CollectionDispatching(_ocDispatcher) мы и так находимся в потоке wpfOcDispatcher. В большинстве случаев излишняя передача параметра sourceDispatcher не приведёт к потере работоспособности, разве что немного пострадает производительность.
Перечисление коллекции-источника происходит также в случае если коллекция-источник передана как обозреваемый аргумент и изменила своё значение.
Обратите внимание на необходимость вызова _ocDispatcher.Dispose().
Обратите внимание DispatcherPriority.Background iпередаётся через параметр destinationOcDispatcherPriority метода расширения CollectionDispatching в метод WpfOcDispatcher.Invoke.
Приведённый выше пример не является единственным вариантом проектирования. Вот ещё один вариант (XAML такой же как в предыдущем примере):
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
WpfOcDispatcher _wpfOcDispatcher;
// OcDispatcher for computations in the background thread
OcDispatcher _ocDispatcher = new OcDispatcher();
public MainWindow()
{
_wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
Orders = new ObservableCollection<Order>();
fillOrdersFromDb();
PaidOrders =
Orders
.Filtering(o => o.Paid)
.CollectionDispatching(_wpfOcDispatcher, _ocDispatcher, (int)DispatcherPriority.Background) // return the computation to the main thread from the background one
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.Paid)
.CollectionDispatching(_wpfOcDispatcher, _ocDispatcher, (int)DispatcherPriority.Background) // return the computation to the main thread from the background one
.For(_consumer);
InitializeComponent();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
_ocDispatcher.Invoke(() => Orders.Add(order));
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocDispatcher.Dispose();
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num)
{
Num = num;
}
public int Num { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.BeginInvoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
И ещё:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
WpfOcDispatcher _wpfOcDispatcher;
public MainWindow()
{
_wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
Orders = new ObservableCollection<Order>();
PaidOrders =
Orders
.Filtering(o => o.Paid)
.CollectionDispatching(_wpfOcDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.Paid)
.CollectionDispatching(_wpfOcDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread
.For(_consumer);
InitializeComponent();
fillOrdersFromDb();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
Orders.Add(order);
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num)
{
Num = num;
}
public int Num { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.BeginInvoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
В предыдущих примерах мы видели как происходит диспетчеризация коллекций средствами метода CollectionDispatching. Но может также возникнуть необходимость в диспетчеризации свойств:
<Window
x:Class="ObservableComputationsExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ObservableComputationsExample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="uc_this"
Title="ObservableComputationsExample"
Width="800"
Height="450"
mc:Ignorable="d"
Closed="mainWindow_OnClosed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label
x:Name="uc_LoadingIndicator"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Left">
Loading source data...
</Label>
<Label
Grid.Row="1"
Grid.Column="0"
FontWeight="Bold">
Unpaid orders
</Label>
<ListBox
Grid.Row="2"
Grid.Column="0"
x:Name="uc_UnpaidOrderList"
DisplayMemberPath="Num"
ItemsSource="{Binding UnpaidOrders, ElementName=uc_this}"
MouseDoubleClick="unpaidOrdersList_OnMouseDoubleClick" />
<Label
Grid.Row="1"
Grid.Column="1"
FontWeight="Bold">
Paid orders
</Label>
<ListBox
Grid.Row="2"
Grid.Column="1"
DisplayMemberPath="Num"
ItemsSource="{Binding PaidOrders, ElementName=uc_this}" />
</Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
// OcDispatcher for computations in the background thread
OcDispatcher _ocDispatcher = new OcDispatcher();
WpfOcDispatcher _wpfOcDispatcher;
public MainWindow()
{
_wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
Orders = new ObservableCollection<Order>();
fillOrdersFromDb();
PaidOrders =
Orders.CollectionDispatching(_ocDispatcher) // direct the computation to the background thread
.Filtering(o => o.PaidPropertyDispatching.Value)
.CollectionDispatching(_wpfOcDispatcher, (int)DispatcherPriority.Background) // return the computation to the main thread
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.Paid)
.For(_consumer);
InitializeComponent();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i, _ocDispatcher, _wpfOcDispatcher);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
this.Dispatcher.Invoke(() => Orders.Add(order), DispatcherPriority.Background);
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void unpaidOrdersList_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
((Order) uc_UnpaidOrderList.SelectedItem).Paid = true;
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocDispatcher.Dispose();
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num, IOcDispatcher backgroundOcDispatcher, IOcDispatcher wpfOcDispatcher)
{
Num = num;
PaidPropertyDispatching = new PropertyDispatching<Order, bool>(this, nameof(Paid), backgroundOcDispatcher, wpfOcDispatcher, 0, (int)DispatcherPriority.Background);
}
public int Num { get; }
public PropertyDispatching<Order, bool> PaidPropertyDispatching { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.BeginInvoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
В этом примере при двойном щелчке мышью по неоплаченному заказу мы делаем его оплаченным. Так как свойство Paid в этом случае меняется в главном потоке, то мы не можем читать его в фоновом потоке _ocOcDispatcher. Для того чтобы читать это свойство в фоновом потоке _ocOcDispatcher, необходимо диспетчеризировать изменения этого свойства в этот поток. Это происходит с помощью класса PropertyDispatching<THolder, TResult>. Аналогично методу CollectionDispatching, конструктор класса PropertyDispatching<THolder, TResult> имеет обязательный параметр destinationOcDispatcher и опциональный параметр sourceOcDispatcher. Отличие в том, что
- вместо перечисления коллекции-источника и подписки на событие CollectionChanged, происходит считывание значения свойства и подписка на событие PropertyChanged.
- значение переданное в параметр sourceOcDispatcher, используется для диспетчеризации изменения значения свойства (сеттер PropertyDispatching<THolder, TResult>.Value) в поток sourceOcDispatcher, в случае если это изменение делается в другом потоке.
Обратите внимание как DispatcherPriority.Background передаётся через параметр sourceOcDispatcherPriority конструктора класса PropertyDispatching в метод WpfOcDispatcher.Invoke.
Приведённый выше пример не является единственным вариантом проектирования. Вот ещё один вариант (XAML не изменился):
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
// OcDispatcher for computations in the background thread
ObservableComputations.OcDispatcher _ocDispatcher = new ObservableComputations.OcDispatcher();
WpfOcDispatcher _wpfOcDispatcher;
public MainWindow()
{
_wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
Orders = new ObservableCollection<Order>();
fillOrdersFromDb();
PaidOrders =
Orders
.Filtering(o => o.PaidPropertyDispatching.Value)
.CollectionDispatching(_wpfOcDispatcher, _ocDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread from the background one
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.PaidPropertyDispatching.Value)
.CollectionDispatching(_wpfOcDispatcher, _ocDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread from the background one
.For(_consumer);
InitializeComponent();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i, _ocDispatcher, _wpfOcDispatcher);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
_ocDispatcher.Invoke(() => Orders.Add(order));
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void unpaidOrdersList_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
((Order) uc_UnpaidOrderList.SelectedItem).Paid = true;
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocDispatcher.Dispose();
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num, IOcDispatcher backgroundOcDispatcher, IOcDispatcher wpfOcDispatcher)
{
Num = num;
PaidPropertyDispatching = new PropertyDispatching<Order, bool>(this, nameof(Paid), backgroundOcDispatcher, wpfOcDispatcher, 0, (int)DispatcherPriority.Background);
}
public int Num { get; }
public PropertyDispatching<Order, bool> PaidPropertyDispatching { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.Invoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
И ещё (XAML не изменился):
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ObservableComputations;
namespace ObservableComputationsExample
{
public partial class MainWindow : Window
{
public ObservableCollection<Order> Orders { get; }
public ObservableCollection<Order> PaidOrders { get; }
public ObservableCollection<Order> UnpaidOrders { get; }
private readonly OcConsumer _consumer = new OcConsumer();
// OcDispatcher for computations in the background thread
OcDispatcher _ocDispatcher = new OcDispatcher();
WpfOcDispatcher _wpfOcDispatcher;
public MainWindow()
{
_wpfOcDispatcher = new WpfOcDispatcher(this.Dispatcher);
Orders = new ObservableCollection<Order>();
PaidOrders =
Orders
.Filtering(o => o.PaidPropertyDispatching.Value)
.CollectionDispatching(_wpfOcDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread
.For(_consumer);
UnpaidOrders =
Orders
.Filtering(o => !o.PaidPropertyDispatching.Value)
.CollectionDispatching(_wpfOcDispatcher, (int)DispatcherPriority.Background) // direct the computation to the main thread
.For(_consumer);
InitializeComponent();
fillOrdersFromDb();
}
private void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i, _ocDispatcher, _wpfOcDispatcher);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
_ocDispatcher.Invoke(() => Orders.Add(order));
}
this.Dispatcher.Invoke(
() => uc_LoadingIndicator.Visibility = Visibility.Hidden,
DispatcherPriority.Background);
});
thread.Start();
}
private void unpaidOrdersList_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
((Order) uc_UnpaidOrderList.SelectedItem).Paid = true;
}
private void mainWindow_OnClosed(object sender, EventArgs e)
{
_ocDispatcher.Dispose();
_consumer.Dispose();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num, IOcDispatcher backgroundOcDispatcher, IOcDispatcher wpfOcDispatcher)
{
Num = num;
PaidPropertyDispatching = new PropertyDispatching<Order, bool>(this, nameof(Paid), backgroundOcDispatcher, wpfOcDispatcher, 0, (int)DispatcherPriority.Background);
}
public int Num { get; }
public PropertyDispatching<Order, bool> PaidPropertyDispatching { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.Invoke(action, (DispatcherPriority)priority);
}
#endregion
}
}
IReadScalar<TValue> впервые был упомянут здесь. Кроме метода CollectionDispatching, ObservableComputations содержит метод ScalarDispatching. Его использование полностью аналогично использованию PropertyDispatching, но с помощью PropertyDispatching Вы можете диспетчеризовать не только свойства. С помощью ScalarDispatching можно организовать диспетчеризацию свойств, но с помощью класса PropertyDispatching<THolder, TResult> она проще и быстрее.
В предыдущих примерах мы увидели как происходит вычисление в одном фоновом потоке. Использую методы диспетчиризации описанные выше есть возможность организовать вычисления в нескольких фоновых потоках, результаты которых конкурентно объединяются в другом потоке (главном или фоновом).
Класса OcDispatcher имеет методы, которые Вы можете вызывать при необходимости
- Invoke* - для синхронного и асинхронного выполнения делегата в потоке экземпляра класса OcDispatcher, например, для изменения исходных данных для вычислений выполняющихся в потоке экземпляра класса OcDispatcher. После вызова метода Dispose данные методы возвращают управление без выполнения переданного делегата и без выброса исключения. Методы имеют параметр setSynchronizationContext. Если установить для этого параметра значение true, то на время восполнения переданного делегата будет установлен синхронизации соответствующий данному вызову. Это может полезно при использовании ключевого слова await внутри делегата.
- InvokeAsyncAwaitable - эти методы возвращают экземпляр классаSystem.Threading.Tasks.Task, и их можно использовать с ключевым словом await.
- ExecuteOtherInvocations - в случае если делегат переданный в метод Invoke* выполняется долго, то Вам может понадобиться вызвать ExecuteOtherInvocations. При вызове ExecuteOtherInvocations вызываются другие делегаты. Есть возможность задать максимальное количество делегатов, которые могут быть выполнены или приблизительное максимальное время их выполнения.
До сих пор мы использовали очень простую реализацию интерфейса IOcDispatcher. Например, такую:
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_dispatcher.Invoke(action, DispatcherPriority.Background);
}
#endregion
}
В этой реализации вызывается метод System.Windows.Threading.Dispatcher.Invoke. В других реализациях мы вызывали System.Windows.Threading.Dispatcher.BeginInvoke. На этом варианты реализации не ограничиваются.
Когда в коллекцию вносится много изменений за короткий промежуток времени, и вы не хотите делать отдельный вызов целевого диспетчера для каждого изменения, а хотите выполнить все изменения за 1 вызов целевого диспетчера (batching), вы можете использовать такую реализацию IOcDispatcher:
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using ObservableComputations;
public class WpfOcDispatcher : IOcDispatcher, IDisposable
{
Subject<Action> _actions;
private System.Windows.Dispatcher _dispatcher;
public WpfOcDispatcher(System.Windows.Dispatcher dispatcher)
{
_dispatcher = dispatcher;
_actions = new Subject<Action>();
_actions.Buffer(TimeSpan.FromMilliseconds(300)).Subscribe(actions =>
{
_dispatcher.Invoke(() =>
{
for (var index = 0; index < actions.Count; index++)
{
actions[index]();
}
}, DispatcherPriority.Background);
});
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_actions.OnNext(action);
}
#endregion
#region Implementation of IDisposable
public void Dispose()
{
_actions.Dispose();
}
#endregion
}
Другой вариант приостановить диспетчер на время изменений:
using System;
using System.Collections.Generic;
using System.Windows.Threading;
using ObservableComputations;
namespace Trader.Domain.Infrastucture
{
public class WpfOcDispatcher : IOcDispatcher
{
private Dispatcher _dispatcher;
public List<Action> _deferredActions = new List<Action>();
private bool _isPaused;
public bool IsPaused
{
get => _isPaused;
set
{
if (_isPaused && !value)
{
_dispatcher.Invoke(() =>
{
foreach (Action deferredAction in _deferredActions)
{
deferredAction();
}
}, DispatcherPriority.Send);
_deferredActions.Clear();
}
_isPaused = value;
}
}
public WpfOcDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
#region Implementation of IDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
if (_isPaused)
{
_deferredActions.Add(action);
return;
}
if (_dispatcher.CheckAccess())
action();
else
_dispatcher.Invoke(action, DispatcherPriority.Send);
}
#endregion
}
}
Пример использования такого диспетчера см. здесь.
При диспетчеризации свойств (PropertyDispatching) и IReadScalar<TValue> (ScalarDispatching) может быть полезен ThrottlingOcDispatcher для подавления слишком частых изменений (например при пользовательском вводе):
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using ObservableComputations;
public class ThrottlingOcDispatcher : IOcDispatcher, IDisposable
{
Subject<Action> _actions;
private System.Windows.Dispatcher _dispatcher;
public WpfOcDispatcher(System.Windows.Dispatcher dispatcher)
{
_dispatcher = dispatcher;
_actions = new Subject<Action>();
_actions.Throttle(TimeSpan.FromMilliseconds(300)).Subscribe(action =>
{
_dispatcher.Invoke(action, DispatcherPriority.Background);
});
}
#region Implementation of IOcDispatcher
public void Invoke(Action action, int priority, object parameter, object context)
{
_actions.OnNext(action);
}
#endregion
#region Implementation of IDisposable
public void Dispose()
{
_actions.Dispose();
}
#endregion
}
Пример использования такого диспетчера см. здесь and здесь.
Класс OcDispatcher может выполнять приоритетную обработку переданных ему делегатов, так же как и WPFs [Dispatcher](https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher?view=net -5,0). По умолчанию OcDispatcher имеет только 1 приоритет, но у конструктора этого класса есть параметр количества возможных приоритетов: prioritiesNumber. В предыдущих примерах вы видели, как установить приоритет пользовательской реализации интерфейса IOcDispatcher (WpfOcDispatcher) в вызовах методов диспетчеризации (CollectionDispatching, ScalarDispatching, PropertyDispatching). Вы можете установить приоритет для экземпляра класса OcDispatcher таким же образом: через параметры destinationOcDispatcherPriority или sourceOcDispatcherPriority методов диспетчеризации. Приоритет по умолчанию самый низкий: 0; Количество или возможные приоритеты OcDispatcher должны быть минимальными, чтобы минимизировать накладные расходы.
Предыдущие примеры были примерами WPF приложения. Аналогичные примеры можно запустить и в консольном приложении. Это может понадобиться для Unit-тестов.
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading;
using ObservableComputations;
namespace ObservableComputationsExamples
{
class Program
{
static ObservableComputations.OcDispatcher _backgroundOcDispatcher = new ObservableComputations.OcDispatcher();
static ObservableComputations.OcDispatcher _mainOcDispatcher = new ObservableComputations.OcDispatcher();
static ObservableCollection<Order> Orders;
static void Main(string[] args)
{
OcConsumer consumer = new OcConsumer();
_mainOcDispatcher.Invoke(() =>
{
ObservableCollection<Order> paidOrders;
ObservableCollection<Order> unpaidOrders;
Orders = new ObservableCollection<Order>();
paidOrders =
Orders.CollectionDispatching(_backgroundOcDispatcher) // direct the computation to the background thread
.Filtering(o => o.PaidPropertyDispatching.Value)
.CollectionDispatching(_mainOcDispatcher,
_backgroundOcDispatcher) // return the computation to the main thread from the background one
.For(consumer);
unpaidOrders = Orders.Filtering(o => !o.Paid).For(consumer);
paidOrders.CollectionChanged += (sender, eventArgs) =>
{
if (eventArgs.Action != NotifyCollectionChangedAction.Add) return;
Console.WriteLine($"Paid order: {((Order) eventArgs.NewItems[0]).Num}" );
};
unpaidOrders.CollectionChanged += (sender, eventArgs) =>
{
if (eventArgs.Action != NotifyCollectionChangedAction.Add) return;
Console.WriteLine($"Unpaid order: {((Order) eventArgs.NewItems[0]).Num}");
};
fillOrdersFromDb();
});
Console.ReadLine();
consumer.Dispose();
_mainOcDispatcher.Dispose();
_backgroundOcDispatcher.Dispose();
}
private static void fillOrdersFromDb()
{
Thread thread = new Thread(() =>
{
Thread.Sleep(1000); // accessing DB
Random random = new Random();
for (int i = 0; i < 5000; i++)
{
Order order = new Order(i, _backgroundOcDispatcher, _mainOcDispatcher);
order.Paid = Convert.ToBoolean(random.Next(0, 3));
_mainOcDispatcher.Invoke(() => Orders.Add(order));
}
});
thread.Start();
}
}
public class Order : INotifyPropertyChanged
{
public Order(int num, IOcDispatcher backgroundOcDispatcher, IOcDispatcher mainOcDispatcher)
{
Num = num;
PaidPropertyDispatching = new PropertyDispatching<Order, bool>(() => Paid, backgroundOcDispatcher, mainOcDispatcher);
}
public int Num { get; }
public PropertyDispatching<Order, bool> PaidPropertyDispatching { get; }
private bool _paid;
public bool Paid
{
get => _paid;
set
{
_paid = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paid)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Описана здесь.
До сих пор мы видели как ObservableComputations отслеживает изменения в значениях свойств и в коллекциях через события PropertyChanged и CollectionChanged. ObservableComputations вводит новый интерфейс и событие для отслеживание значений возвращаемых методами: интерфейс INotifyMethodChanged и событие MethodChanged. Вот пример:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class RoomReservation
{
public string RoomId { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
}
public class RoomReservationManager : INotifyMethodChanged
{
private List<RoomReservation> _roomReservations = new List<RoomReservation>();
public void AddReservation(RoomReservation roomReservation)
{
_roomReservations.Add(roomReservation);
MethodChanged?.Invoke(this, new MethodChangedEventArgs(
nameof(IsRoomReserved),
args =>
{
string roomId = (string) args[0];
DateTime dateTime = (DateTime) args[1];
return
roomId == roomReservation.RoomId
&& roomReservation.From < dateTime && dateTime < roomReservation.To;
}));
}
public bool IsRoomReserved(string roomId, DateTime dateTime)
{
return _roomReservations.Any(rr =>
rr.RoomId == roomId
&& rr.From < dateTime && dateTime < rr.To);
}
public event EventHandler<MethodChangedEventArgs> MethodChanged;
}
public class Meeting : INotifyPropertyChanged
{
private string _roomNeeded;
public string RoomNeeded
{
get => _roomNeeded;
set
{
_roomNeeded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RoomNeeded)));
}
}
private DateTime _dateTimeNeeded;
public DateTime DateTimeNeeded
{
get => _dateTimeNeeded;
set
{
_dateTimeNeeded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DateTimeNeeded)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
OcConsumer consumer = new OcConsumer();
RoomReservationManager roomReservationManager = new RoomReservationManager();
Meeting planingMeeting = new Meeting()
{
RoomNeeded = "ConferenceHall",
DateTimeNeeded = new DateTime(2020, 02, 07, 15, 45, 00)
};
Computing<bool> isRoomReservedComputing =
new Computing<bool>(() =>
roomReservationManager.IsRoomReserved(
planingMeeting.RoomNeeded,
planingMeeting.DateTimeNeeded))
.For(consumer);
isRoomReservedComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<bool>.Value))
{
// see changes here
}
};
roomReservationManager.AddReservation(new RoomReservation()
{
RoomId = "ConferenceHall",
From = new DateTime(2020, 02, 07, 15, 00, 00),
To = new DateTime(2020, 02, 07, 16, 00, 00)
});
planingMeeting.DateTimeNeeded = new DateTime(2020, 02, 07, 16, 30, 00);
Console.ReadLine();
consumer.Dispose();
}
}
}
Как вы видите MethodChangedEventArgs содержит свойство ArgumentsPredicate. Следующее значение передаётся в это свойство:
args =>
{
string roomId = (string) args[0];
DateTime dateTime = (DateTime) args[1];
return
roomId == roomReservation.RoomId
&& roomReservation.From < dateTime && dateTime < roomReservation.To;
}
Данное свойство определяет какие значения должны иметь аргументы в вызове метода, так чтобы возвращаемое значение этого вызова изменилось.
Внимание: Пример кода в этом разделе не является образцом проектирования, это скорее антипаттерн: он содержит дублирование кода и изменения в свойствах класса RoomReservation не отслеживаются. Этот код приведён только для демонстрации отслеживание значений возвращаемых методом. См. исправленный код здесь.
INotifyMethodChanged реализуют следующие вычисление
- Dictionaring (методы: ContainsKey, Indexer ([]), GetValueOrDefault).
- ConcurrentDictionaring (методы: ContainsKey, Indexer([]), GetValueOrDefault).
- HashSetting (метод: Contains).
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private string _type;
public string Type
{
get => _type;
set
{
_type = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Type)));
}
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Type = "VIP"},
new Order{Num = 2, Type = "Regular"},
new Order{Num = 3, Type = "VIP"},
new Order{Num = 4, Type = "VIP"},
new Order{Num = 5, Type = "NotSpecified"},
new Order{Num = 6, Type = "Regular"},
new Order{Num = 7, Type = "Regular"}
});
ObservableCollection<string> selectedOrderTypes = new ObservableCollection<string>(new []
{
"VIP", "NotSpecified"
});
OcConsumer consumer = new OcConsumer();
ObservableCollection<Order> filteredByTypeOrders =
orders.Filtering(o =>
selectedOrderTypes.ContainsComputing(
() => o.Type).Value)
.For(consumer);
filteredByTypeOrders.CollectionChanged += (sender, eventArgs) =>
{
// see the changes (add, remove, replace, move, reset) here
};
// Start the changing...
orders.Add(new Order{Num = 8, Type = "VIP"});
orders.Add(new Order{Num = 9, Type = "NotSpecified"});
orders[4].Type = "Regular";
orders.Move(4, 1);
orders[0] = new Order{Num = 10, Type = "Regular"};
selectedOrderTypes.Remove("NotSpecified");
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде выше selectedOrderTypes.ContainsComputing(() => o.Type) является вложенным вычислением, которое зависит от внешнего параметра o. Эти два обстоятельства приводят к тому, что экземпляр класса ContainsComputing будет создан для каждого заказа в коллекции orders . Это может повлиять на производительность и потребление памяти, если количество заказов велико. К счастью, вычисление filteredByTypeOrders может быть сделано "плоским":
ObservableCollection<Order> filteredByTypeOrders = orders
.Joining(selectedOrderTypes, (o, ot) => o.Type == ot)
.Selecting(oot => oot.OuterItem);
Это вычисление имеет преимущество в производительности и потреблении памяти.
Предположим мы имеем долго вычисляемой свойство и хотим увеличить производительность при получении его значения:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class ValueHolder : INotifyPropertyChanged
{
private string _value;
public string Value
{
get
{
Thread.Sleep(100);
return _value;
}
set
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
private Computing<string> _valueComputing;
public Computing<string> ValueComputing => _valueComputing =
_valueComputing ?? new Computing<string>(() => Value);
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
ValueHolder valueHolder = new ValueHolder();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < 20; i++)
{
string value = valueHolder.Value;
}
stopwatch.Stop();
Console.WriteLine($"Direct access to property: {stopwatch.ElapsedMilliseconds}");
stopwatch.Restart();
for (int i = 0; i < 20; i++)
{
string value = valueHolder.ValueComputing.Value;
}
stopwatch.Stop();
Console.WriteLine($"Access to property via computing: {stopwatch.ElapsedMilliseconds}");
Console.ReadLine();
}
}
}
Код выше имеет следующий вывод:
Direct access to property: 2155
Access to property via computing: 626
Метод расширения Differing<TResult>
Этот метод позволяет Вам подавить лишние вызовы события PropertyChanged (когда значение свойства не изменилось).
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Angle : INotifyPropertyChanged
{
private double _rads;
public double Rads
{
get
{
return _rads;
}
set
{
_rads = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Rads)));
}
}
public static double DegreesToRads(double degrees) => degrees * (Math.PI / 180);
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Angle angle = new Angle(){Rads = Angle.DegreesToRads(0)};
OcConsumer consumer = new OcConsumer();
Computing<double> sinComputing =
new Computing<double>(
() => Math.Round(Math.Sin(angle.Rads), 3)) // 0
.For(consumer);
Console.WriteLine($"sinComputing: {sinComputing.Value}");
sinComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<double>.Value))
{
Console.WriteLine($"sinComputing: {sinComputing.Value}");
}
};
Differing<double> differingSinComputing =
sinComputing.Differing().For(consumer);
Console.WriteLine($"differingSinComputing: {sinComputing.Value}");
differingSinComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<double>.Value))
{
Console.WriteLine($"differingSinComputing: {differingSinComputing.Value}");
}
};
angle.Rads = Angle.DegreesToRads(30); // 0,5
angle.Rads = Angle.DegreesToRads(180) - angle.Rads; // 0,5
angle.Rads = Angle.DegreesToRads(360 + 180) - angle.Rads; // 0,5
angle.Rads = Angle.DegreesToRads(360) - angle.Rads; // -0,5
Console.ReadLine();
consumer.Dispose();
}
}
}
Код выше имеет следующий вывод:
sinComputing: 0
differingSinComputing: 0
sinComputing: 0,5
differingSinComputing: 0,5
sinComputing: 0,5
sinComputing: 0,5
sinComputing: -0,5
differingSinComputing: -0,5
Иногда обработка каждого события PropertyChanged занимает много времени и может подвесить пользовательский интерфейс (перерисовка, перевычисление). Используйте метод расширения Differing, чтобы уменьшить этот эффект.
Если после инстанцирования класса вычисления коллекции (напр. Filtering), ожидается что коллекция значительно вырастет, имеет смысл передать в конструктор аргумент capacity, чтобы зарезервировать память под коллекцию.
См. подробности здесь.
Если Вам необходимо произвести много изменений в исходных данных и Вы не хотите обрабатывать каждое изменение в Ваших вычислениях, Вы можете временно приостановить вычисление (поставить на паузу).
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Num {get; set;}
private decimal _price;
public decimal Price
{
get => _price;
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
}
class Program
{
static void Main(string[] args)
{
ObservableCollection<Order> orders =
new ObservableCollection<Order>(new []
{
new Order{Num = 1, Price = 15},
new Order{Num = 2, Price = 15},
new Order{Num = 3, Price = 25},
new Order{Num = 4, Price = 27},
new Order{Num = 5, Price = 30},
new Order{Num = 6, Price = 75},
new Order{Num = 7, Price = 80}
});
// We start using ObservableComputations here!
OcConsumer consumer = new OcConsumer();
CollectionPausing<Order> ordersPausing = orders.CollectionPausing();
Filtering<Order> expensiveOrders =
ordersPausing
.Filtering(o => o.Price > 25)
.For(consumer);
Debug.Assert(expensiveOrders is ObservableCollection<Order>);
checkFiltering(orders, expensiveOrders); // Prints "True"
expensiveOrders.CollectionChanged += (sender, eventArgs) =>
{
// see the changes (add, remove, replace, move, reset) here
checkFiltering(orders, expensiveOrders); // Prints "True"
};
// Start the changing...
orders.Add(new Order{Num = 8, Price = 30});
orders.Add(new Order{Num = 9, Price = 10});
// Start many changes...
ordersPausing.IsPaused = true;
for (int i = 10; i < 1000; i++)
orders.Add(new Order{Num = i, Price = 30});
checkFiltering(orders, expensiveOrders); // Prints "False"
ordersPausing.IsPaused = false;
checkFiltering(orders, expensiveOrders); // Prints "True"
Console.ReadLine();
consumer.Dispose();
}
static void checkFiltering(
ObservableCollection<Order> orders,
Filtering<Order> expensiveOrders)
{
Console.WriteLine(expensiveOrders.SequenceEqual(
orders.Where(o => o.Price > 25)));
}
}
}
Обратите внимание, что во время вызова "ordersPausing.IsPaused = false;" ordersPausing генерирует событиеCollectionChanged с NotifyCollectionChangedAction.Reset. Это поведение по умолчанию. Вы можете установить значение параметра resumeType метода расширения CollectionPausing в CollectionPausingResumeType.ReplayChanges и вместо NotifyCollectionChangedAction.Reset ordersPausing воспроизведёт всю последовательность изменений, которые были сделаны за время паузы. ObservableComputations также включает в себя метод расширения ScalarPausing. Его использование аналогично. Вместо CollectionPausingResumeType ScalarPausing позволяет установить сколько последних изменений будет воспроизведено при возобновлении. Значение по умолчанию 1. null соответствует всем изменениям.
Если некоторое вычисление необходимо только для некоторых сценариев, либо Вы ходите отложить инициализацию до того момента когда вычисление потребуется, Вам подходит ленивая инициализация. Вот пример:
private Computing<string> _valueComputing;
public Computing<string> ValueComputing => _valueComputing =
_valueComputing ?? new Computing<string>(() => Value).For(_consumer);
Пример кода приведённый в разделе "Отслеживание значений возвращаемых методом" не является образцом проектирования, это скорее антипаттерн: он содержит дублирование кода и изменения в свойствах класса RoomReservation не отслеживаются. Этот код приведён только для демонстрации отслеживание значений возвращаемых методом. Вот код с исправленным дизайном:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class RoomReservation : INotifyPropertyChanged
{
private string _roomId;
public string RoomId
{
get => _roomId;
set
{
_roomId = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RoomId)));
}
}
private DateTime _from;
public DateTime From
{
get => _from;
set
{
_from = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(From)));
}
}
private DateTime _to;
public DateTime To
{
get => _to;
set
{
_to = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(To)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class RoomReservationManager
{
private ObservableCollection<RoomReservation> _roomReservations = new ObservableCollection<RoomReservation>();
private ReadOnlyObservableCollection<RoomReservation> _roomReservationsReadOnly;
public RoomReservationManager()
{
_roomReservationsReadOnly = new ReadOnlyObservableCollection<RoomReservation>(_roomReservations);
}
public void AddReservation(RoomReservation roomReservation)
{
_roomReservations.Add(roomReservation);;
}
public ReadOnlyObservableCollection<RoomReservation> RoomReservations =>
_roomReservationsReadOnly;
}
public class Meeting : INotifyPropertyChanged
{
private string _roomNeeded;
public string RoomNeeded
{
get => _roomNeeded;
set
{
_roomNeeded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RoomNeeded)));
}
}
private DateTime _dateTimeNeeded;
public DateTime DateTimeNeeded
{
get => _dateTimeNeeded;
set
{
_dateTimeNeeded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DateTimeNeeded)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
RoomReservationManager roomReservationManager = new RoomReservationManager();
Meeting planingMeeting = new Meeting()
{
RoomNeeded = "ConferenceHall",
DateTimeNeeded = new DateTime(2020, 02, 07, 15, 45, 00)
};
OcConsumer consumer = new OcConsumer();
AnyComputing<RoomReservation> isRoomReservedComputing =
roomReservationManager.RoomReservations.AnyComputing<RoomReservation>(rr =>
rr.RoomId == planingMeeting.RoomNeeded
&& rr.From < planingMeeting.DateTimeNeeded
&& planingMeeting.DateTimeNeeded < rr.To)
.For(consumer);
isRoomReservedComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<bool>.Value))
{
// see changes here
}
};
roomReservationManager.AddReservation(new RoomReservation()
{
RoomId = "ConferenceHall",
From = new DateTime(2020, 02, 07, 15, 00, 00),
To = new DateTime(2020, 02, 07, 16, 00, 00)
});
planingMeeting.DateTimeNeeded = new DateTime(2020, 02, 07, 16, 30, 00);
Console.ReadLine();
consumer.Dispose();
}
}
}
Обратите внимание на то, что тип RoomReservationManager._roomReservations изменён на ObservableCollection<RoomReservation> и было добавлено свойство RoomReservationManager.RoomReservations типа System.Collections.ObjectModel.ReadOnlyObservableCollectionn<RoomReservation>.
См. здесь
Области применения метода расширения Using<TResult>
См. конец раздела Отслеживание произвольного выражения.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class OrderLine : INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get
{
return _price;
}
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Order : INotifyPropertyChanged
{
public ObservableCollection<OrderLine> Lines = new ObservableCollection<OrderLine>();
public OcConsumer Consumer;
private decimal _discount;
public decimal Discount
{
get
{
return _discount;
}
set
{
_discount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Discount)));
}
}
private Computing<decimal> _priceWithDiscount;
public Computing<decimal> PriceWithDiscount
{
get
{
if (_priceWithDiscount == null)
{
// first step
Summarizing<decimal> totalPrice
= Lines.Selecting(l => l.Price).Summarizing();
// second step
_priceWithDiscount =
new Computing<decimal>(
() => totalPrice.Value - totalPrice.Value * Discount)
.For(Consumer);
}
return _priceWithDiscount;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
OcConsumer consumer = new OcConsumer();
Order order = new Order(){Discount = 0.25m, Consumer = consumer};
order.Lines.Add(new OrderLine(){Price = 100});
order.Lines.Add(new OrderLine(){Price = 150});
order.Lines.Add(new OrderLine(){Price = 50});
Console.WriteLine(order.PriceWithDiscount.Value);
order.Lines[1].Price = 130;
Console.WriteLine(order.PriceWithDiscount.Value);
Console.ReadLine();
consumer.Dispose();
}
}
}
Обратите внимание на свойство PriceWithDiscount. В теле этого свойства мы конструируем вычисление _priceWithDiscount в два шага. Можем ли мы переписать свойство PriceWithDiscount, чтобы оно стало expression body членом? Да:
public Computing<decimal> PriceWithDiscount => _priceWithDiscount = _priceWithDiscount ??
Lines.Selecting(l => l.Price).Summarizing().Using(p => p.Value - p.Value * Discount).For(Consumer);
В коде выше p это результат Lines.Selecting(l => l.Price).Summarizing(). Поэтому параметр p похож на переменную. Следующий код не корректен так как изменения в свойстве OrderLine.Price и коллекции Order.Lines не отражаются в результирующем вычислении:
public Computing<decimal> PriceWithDiscount => _priceWithDiscount = _priceWithDiscount ??
Lines.Selecting(l => l.Price).Summarizing().Value.Using(p => p - p * Discount).For(Consumer);
В этом коде параметр p имеет тип decimal, а не Summarizing<decimal> как в корректном варианте. См. подробности [здесь](#передача-аргументов-как-обозреваемых-и-не-обозреваемых.
IReadScalar<TValue> упоминается в первый раз здесь. Не существует встроенных средств для получение предыдущего значения свойства во время обработки PropertyChanged event. ObservableComputations приходит на помощь и предоставляет методы расширения PreviousTracking<TResult> и WeakPreviousTracking<TResult>.
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private string _deliveryDispatchCenter;
public string DeliveryDispatchCenter
{
get
{
return _deliveryDispatchCenter;
}
set
{
_deliveryDispatchCenter = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DeliveryDispatchCenter)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Order order = new Order()
{
DeliveryDispatchCenter = "A"
};
OcConsumer consumer = new OcConsumer();
PreviousTracking<string> previousTracking =
new Computing<string>(() => order.DeliveryDispatchCenter)
.PreviousTracking()
.For(consumer);
previousTracking.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(Computing<double>.Value))
{
Console.WriteLine($"Current dispatch center: {previousTracking.Value}; Previous dispatch center: {previousTracking.PreviousValue};");
}
};
order.DeliveryDispatchCenter = "B";
order.DeliveryDispatchCenter = "C";
Console.ReadLine();
consumer.Dispose();
}
}
}
Код выше имеет следующий вывод:
Current dispatch center: B; Previous dispatch center: A;
Current dispatch center: C; Previous dispatch center: B;
Обратите внимание на то, что свойство PreviousValue можно отслеживать через событие PropertyChanged event, поэтому Вы можете включить его в Ваши обозреваемые вычисления.
Обратите внимание на то, что PreviousTracking<TResult> имеет сильную ссылку на значение TResult (свойство PreviousValue) (в случае если TResult является ссылочным типом). Учтите это когда будете думать о сборке мусора и утечках памяти. WeakPreviousTracking<TResult> может помочь Вам. Вместо свойства PreviousValue WeakPreviousTracking<TResult> включает в себя метод TryGetPreviousValue. Изменения в возвращаемом значении этого метода не могут отслеживаться, поэтому Вы не можете включить его в свои обозреваемые вычисления.
Следующий код будет работать некорректно:
using System;
using System.ComponentModel;
using System.Reflection;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get
{
return _price;
}
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Order order = new Order()
{
Price = 1
};
PropertyInfo pricePropertyInfo = typeof(Order).GetProperty(nameof(Order.Price));
OcConsumer consumer = new OcConsumer();
Computing<decimal> priceReflectedComputing =
new Computing<decimal>(() => (decimal)pricePropertyInfo.GetValue(order))
.For(consumer);
priceReflectedComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(PropertyAccessing<decimal>.Value))
{
Console.WriteLine(priceReflectedComputing.Value);
}
};
order.Price = 2;
order.Price = 3;
Console.ReadLine();
consumer.Dispose();
}
}
}
Код выше не имеет вывода, так как изменения в значения возвращаемого методом GetValue не могут быть отслежены. Вот исправленный код:
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get
{
return _price;
}
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Order order = new Order()
{
Price = 1
};
OcConsumer consumer = new OcConsumer();
PropertyAccessing<decimal> priceReflectedComputing =
order.PropertyAccessing<decimal>(nameof(Order.Price))
.For(consumer);
priceReflectedComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(PropertyAccessing<decimal>.Value))
{
Console.WriteLine(priceReflectedComputing.Value);
}
};
order.Price = 2;
order.Price = 3;
Console.ReadLine();
consumer.Dispose();
}
}
}
В коде выше мы используем метод расширения PropertyAccessing. Убедитесь, что Вы ознакомились с Передача аргументов как обозреваемых и не обозреваемых: в коде выше первый аргумент (order) в методе расширения PropertyAccessing передан как не обозреваемые. В следующем коде этот аргумент передаётся как observable.
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
private decimal _price;
public decimal Price
{
get
{
return _price;
}
set
{
_price = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Manager : INotifyPropertyChanged
{
private Order _processingOrder;
public Order ProcessingOrder
{
get
{
return _processingOrder;
}
set
{
_processingOrder = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProcessingOrder)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
class Program
{
static void Main(string[] args)
{
Order order = new Order()
{
Price = 1
};
Manager manager = new Manager(){ProcessingOrder = order};
OcConsumer consumer = new OcConsumer();
PropertyAccessing<decimal> priceReflectedComputing =
new Computing<Order>(() => manager.ProcessingOrder)
.PropertyAccessing<decimal>(nameof(Order.Price))
.For(consumer);
priceReflectedComputing.PropertyChanged += (sender, eventArgs) =>
{
if (eventArgs.PropertyName == nameof(PropertyAccessing<decimal>.Value))
{
Console.WriteLine(priceReflectedComputing.Value);
}
};
order.Price = 2;
order.Price = 3;
manager.ProcessingOrder =
new Order()
{
Price = 4
};
Console.ReadLine();
consumer.Dispose();
}
}
}
Следующий код не будет работать корректно, так как изменения в manager.ProcessingOrder не будут отражаться в priceReflectedComputing, так как первый аргумент (manager.ProcessingOrder) в методе расширения PropertyAccessing передан как не обозреваемый:
PropertyAccessing<decimal> priceReflectedComputing
= manager.ProcessingOrder.PropertyAccessing<decimal>(nameof(Order.Price)).For(consumer);
Если ссылка на объект, у которого вычисляется значение свойства, является null, то PropertyAccessing<TResult>.Value возвращает значение по умолчанию для TResult. Вы можете изменить это значение передавая параметр defaultValue.
Класс и метод расширения Binding позволяет связать два произвольных выражения. Первое выражение это источник. Второе выражение является целевым. Сложность выражений не ограничена. Первое выражение передаётся как дерево выражений. Второе выражение передаётся как делегат. Когда значение выражения источника меняется, новое значение присваивается целевому выражению:
using System;
using System.ComponentModel;
using ObservableComputations;
namespace ObservableComputationsExamples
{
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _deliveryAddress;
public string DeliveryAddress
{
get => _deliveryAddress;
set
{
_deliveryAddress = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DeliveryAddress)));
}
}
}
public class Car : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _destinationAddress;
public string DestinationAddress
{
get => _destinationAddress;
set
{
_destinationAddress = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DestinationAddress)));
}
}
}
class Program
{
static void Main(string[] args)
{
Order order = new Order(){DeliveryAddress = ""};
Car assignedDeliveryCar = new Car(){DestinationAddress = ""};
Binding<string> deliveryAddressBinding = new Binding<string>(
() => order.DeliveryAddress,
da => assignedDeliveryCar.DestinationAddress = da);
Console.WriteLine(assignedDeliveryCar.DestinationAddress);
order.DeliveryAddress = "A";
Console.WriteLine(assignedDeliveryCar.DestinationAddress);
order.DeliveryAddress = "B";
Console.WriteLine(assignedDeliveryCar.DestinationAddress);
Console.ReadLine();
deliveryAddressBinding.Dispose();
}
}
}
В коде выше мы связываем order.DeliveryAddress и assignedDeliveryCar.DestinationAddress. order.DeliveryAddress является источником связывания. assignedDeliveryCar.DestinationAddress является целью связывания.
Метод расширения Binding расширяет IReadScalar<TValue>, экземпляр которого является источником связывания.
Могу ли я использовать IList<T> с ObservableComputations?
Если у Вас есть коллекция реализующая IList<T>, но не реализующая INotifyColectionChanged (на пример List<T>), Вы можете использовать её с ObservableComputations. См.
https://github.com/gsonnenf/Gstc.Collections.ObservableLists
Nuget: https://www.nuget.org/packages/Gstc.Collections.ObservableLists