Feeds architecture
This document gives information about the architecture and the implementation of feeds. To get information about the usage off the feeds framework, you should look to this doc.
Dev general guidelines
- All instances of feeds should be cached using the
AttachedPropertyhelper. - Consequently, all feed implementation must be state less (except the special case of
State<T>). - When invoking a user asynchronous method, the
SourceContextshould be set as ambient (cf.FeedHelper.InvokeAsync). - Untyped interfaces (
IMessage,IMessageEntry, etc.) exists only for binding consideration and should not be re-implemented nor used in source code.
Caching
In order to allow a light creation syntax in a property getter (public Feed<int> MyFeed => _anotherFeed.Select(_ => 42), which is re-evaluated each time the property is get), but without rebuilding and re-querying the feed each time, we have 2 levels of caching.
Instance caching
First is the instance of the feed itself. This is done using the AttachedProperty helper class. Each feed if attached to a owner and identified by a key. It makes sure that for a given owner we have only one instance of the declared feed, i.e. running _anotherFeed.Select(_ => 42) will always return the same instance.
The owner is usually (by order of preference):
- The parent feed if any (
_anotherFeedin example above); - The
Targetof thekeydelegate, so the instance of the class that is declaring the feed; - The
keydelegate itself if it’s a static delegate instance (e.g., in the example above_ => 42theTargetis going to benullas we don’t have any capture)
The key is usually the delegate that is provided by the user. It’s really important here to note that a helper method like below would break the instance caching:
public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
=> feed.Select(t => toString(t));
As the delegate t => toString(t) is declared in the method itself, it will be re-instantiated each time. Valid implementations would have been:
public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
=> feed.Select(toString); // We are directly forwarding the user delegate
public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
=> AttachedProperty.GetOrCreate(owner: feed, key: toString, factory: (theFeed, theToString) => theFeed.Select(t => theToString(t))); // We are explicitly caching the instance.
Subscription caching
The second level of caching is for the “subscription” on a feed. This is needed to make sure that in a given context, enumerating / awaiting multiple times to a same feed won’t re-build the value (noticeably, won’t re-request a value coming from a web API call).
This caching is achieved by the SourceContext.
Those context are weakly attached to a owner (typically a ViewModel) and each call to context.GetOrCreateSource(feed) will return a state full subscription to the feed which will replay the last received message.
When implementing an
IFeed, thecontextprovided in theGetSourceis only intended to be used to restore it as current in some circumstances, like invoking a user’s async method. Your feed must remain state less, so you should not usecontext.GetOrCreateSource(parent).On the other side, each helper that allow user to “subscribe” to a feed should do something like
SourceContext.Current.GetOrCreateSource(feed)(and notfeed.GetSource(SourceContext.Current))
Issuing messages
When implementing an IFeed you will have to create some messages.
If you don't have any parent feed the easiest way is to start from Message<T>.Initial (do not send it as first message), then update it:
var current = Message<int>.Initial;
for (var i = 0; i++; i < 42)
{
yield return i;
await Task.Delay(100, ct);
}
If you do have a parent feed, you should use the MessageManager<TParent, TResult>, eg.:
var manager = new MessageManager<TParent, TResult>();
var msgIndex = 0;
await foreach(var parentMsg in _parent.GetSource(context))
{
if (manager.Update(localMsg => localMsg.With(parentMsg).Data(msgIndex++)))
{
yield return manager.Current;
}
}
Make sure that your feed always produces at least on message. If there isn’t any relevant, send the
Message.Initialbefore completing theIAsyncEnumerablesource.If you have a parent feed make sure to always forward the parent message, even if the parent message does not change any local value:
manager.Update(localMsg => localMsg.With(parentMsg)).Be aware that enumeration of an
IAsyncEnumerableis sequential (i.e. one value at once). TheMessageManageralso has a constructor that allows to asynchronously send messages to anAsyncEnumerableSubject.
Axes
An axe is referring to an “informational axe” related to a given data, a.k.a. a metadata. Currently the feed framework is managing (i.e., actively generating value for) only 2 metadata: error and progress, but as Messages are designed to encapsulate a data and all its metadata, a MessageEntry can have more than those 2 well-known axes.
┌────┐1 *┌────────┐ 2┌────────────┐1 *┌────────────────┐
│Feed├────►│Message │ ┌─►│MessageEntry├────►│MessageAxis │
└────┘ ├────────┤ │ ├────────────┤ └─────┬──────────┘
│Previous├──┤ │Data │1 │1
│Current ├──┘ │Error ├──┐ │
│Changes │ │Progress │ │ │1
└────────┘ └────────────┘ │ *┌─────┴──────────┐
└─►│MessageAxisValue│
└────────────────┘
MessageEntryare basically dictionaries ofMessageAxisandMessageAxisValue, except they are returningMessageAxisValue.Unsetinstead of throwing error for unset axes.The
DataAxisis a special axis that must be set on all entries. It exists only to unify/ease implementation. You should useOption.Undefinedto “unset” the data.If you are about to add an axe, you should make sure to provided extensions methods over
IMessageBuilderandIMessageEntryto read/write it directly to/from the effective metadata type. The genericGetandSetare there for that and should not be used directly in user’s code.
Request
The subscriber of a feed can send some request to the source feed to enhance its behavior.
The most common request is the RefreshRequest.
When implementing an
IFeedyou have access to those requests using theRequests<TRequest>()method on theSourceContextyou get in theGetSource.When consuming a feed, you can send a request to that feed by creating a "child" context (
SourceContext.CreateChild()) giving you ownIRequestSource.
Token and TokenSet
In a response to a request a feed might issue a token that is then added to its messages so the subscriber that sent the request is able to track the completion of the request. This is the case for the Refresh and the Pagination which are forwarding those tokens through the refresh and the pagintation axes.
As when a subscriber sends a request, it might be handled by more than one feed. For instance, when combining two AsyncFeed instances, the refresh request will cause those two feeds to refresh their data.
Even if it's not yet the implemented, we can also imagine that an operator feed (such as the SelectAsyncFeed) might also trigger a refresh of its own projection.
Refresh and Pagination axes are working with TokenSet. It is a collection of IToken that only keep the latest tokens for a given source in relation to the subscription context.
Subscriber Combine Async A Async B
│ │ │ │
┌─ Control channel (Requests on SourceContext) ─┐
│ │ │ │ │ │
│ │ Request 1 │ │ │ │
┌──┼──◄─┼──────────────►│ Request 1 │ │ │
│ │ │ ├──────────►│ │ │
▼ │ │ │ Token A │ │ │
│ │ │ │◄──────────┘ │ │
│ │ │ │ Request 1 │ │
▼ │ │ ├──────────────────────►│ │
│ │ │ │ Token B │ │
│ │ │ │◄──────────┬───────────┤ │
▼ │ │ TokenSet[A,B] │ │ │ │
│ │ │◄──────────────┤ │ │ │
│ │ │ │ │ │ │
▼ └────┼───────────────┼───────────┼───────────┼──┘
IsExecuting │ │ │ │
│ ┌─ Message channel ──┼───────────┼───────────┼──┐
│ │ │ │ │ │ │
▼ │ │ ┤ Msg with │ │ │
│ │ │ │ token A │ │ │
│ │ │ │◄──────────┤ Msg with │ │
▼ │ │ Msg with │ │ token A │ │
│ │ │ TokenSet[A,B] │◄──────────┼───────────┤ │
└─►├───►│◄──────────────┤ │ │ │
│ │ │ │ │ │
└────┼───────────────┼───────────┼───────────┼──┘
│ │ │ │
Note
As a subscriber, you can use the TokenSetAwaiter to wait for a set that has been produced by a request.
Note
When implementing an IFeed you can use the CoercingRequestManager or the SequentialRequestManager to easily implement such request / token logic.
When you answer to a request with a token, you must then issue a new message with that token.
View
While developing in the feed framework, the most interesting view-related part is the presentation layer. Feeds are transferring messages containing the data and its metadata, but those are not binding friendly. The presentation layer has then 2 responsibilities:
- Read, i.e. from the ViewModel to the View, it will expose and maintain the state of the last message through standard binding-friendly properties and
INotifyPropertyChanged; - Write, i.e. from the View to the ViewModel, it will convert back standard properties into so called
IInput, which inherits from theIFeedinterface, and which allows the ViewModel to manage the view just like any other data source.
Those binding friendly properties are generated by the FeedsGenerator.
To trigger the generation, the class needs to have at least on constructor which has an
IInput, or anICommandBuilderparameter, or flag the type with[ReactiveBindable(true)]attribute.When we generate a binding friendly class for a type
MyTypewe are generating aBindableMyTypeclass nested into theMyTypeitself. This class inherits from theBindableViewModelBase.
Denormalizing
In order to make a record editable field per field (e.g. a user profile where you want to edit first name and last name independently), we can generate a BindableRecord object that we re-expose each property of the Record independently.
When a record is denormalized, we are generating / maintaining only one
State<Record>. Setting a value of any of the denormalized properties, will update that root state.When a record is denormalized, we can no longer directly set a record instance on the
VM.Record(the property is of typeBindableRecord). To work around this, if the record does not have aValueproperty which would conflict, we are generating a get/set propertyValuewhich can be used to set the instance (e.g.VM.Record.Value).When a property is updated, we also ensure to raise the property changes, including for the
Valueproperty (if defined) and theGetValuemethod.
Dispatching and threading considerations
The user’s ViewModel are expected to run on background thread for performance considerations. Commands and state updates from generated ViewModel classes are always ran on a background thread.
To avoid strong dependency on a dispatcher (neither by injection nor by the need to create VM on UI thread), all generated classes are auto resolving the dispatcher when an event handler is registered or when a public method is used, using the internal LazyDispatcherResolver and EventManager. Those classes are then completing their initialization once the first dispatcher is resolved.
The EventManager will capture the thread used to register an event handler and will then make sure to invoke that handler on that thread if it was a UI thread. This allows BindableVM to be data-bound to multiple window/UI threads.
┌───────────────┐ ┌───────────────┐
│EventManager │ │IInvocationList│
├───────────────┤1 *├───────────────┤
│Add(handler) ├────►│Add(handler) │
│Remove(handler)│ │Remove(handler)│
│Raise(args) │ │Raise(args) │
└───────────────┘ └───────▲───────┘
│
┌───────────────┴──────────────┐
│ │
┌───────────┴────────────┐ ┌────────────┴────────────┐
│DispatcherInvocationList│ │MultiThreadInvocationList│
└───────────▲────────────┘ └─────────────────────────┘
│
┌─────┴───────┐
│ │
┌─────┴─────┐ ┌───┴────┐
│Coaslescing│ │Queueing│
└───────────┘ └────────┘
We have one
IInvocationListper UI thread and one for all background threads (theMultiThreadInvocationList).As a bonus, when a dispatcher bound handler is about to be invoked from another thread, it can be coalesced to be raised only once (cf.
CoalescingDispatcherInvocationList).