What are feeds?

Feeds are there to manage asynchronous operations (for example requesting data from a service) and expose the result to the View in an efficient manner.

They provide out of the box support for task-based methods as well as Async-Enumerables ones.

Feeds include additional metadata that indicates whether the operation is still in progress, ended in an error, or if it was successful, whether the data that was returned contains any entries or was empty.

Feeds are stateless

Feeds are typically used to request data from services and expose it in a stateless manner so that the resulting data can be displayed by the View.

Feeds are stateless and do not provide support for reacting to changes the user makes to the data on the View. The data can only be reloaded and refreshed upon request which is when the underlying task or Async-Enumerable will be invoked and the data refreshed. In other words, a feed is a read-only representation of the data received from the server.

In contrast to feeds, states (IState or IListState), as the name suggests, are stateful and keep track of the latest value, as updates are applied.

How to use feeds?

Creation of feeds

For the examples below, let's use a counter service that returns the current count number, starting from 1. It will be run 3 consecutive times delayed by a second each. For the data type, we'll create a record type called CounterValue:

public record CounterValue(int Value);

Feeds can be created directly from methods that return a ValueTask or Task, or from methods that return an IAsyncEnumerable. In both cases, the methods can optionally take a CancellationToken parameter.

Feed.Async factory

The Feed.Async factory method will create an IFeed by invoking a method that will return either a ValueTask or a Task. For example, the CountOne method will wait for a second (unless cancelled via the CancellationToken) before returning the next counter value.

private int _currentCount = 0;

public async ValueTask<CounterValue> CountOne(CancellationToken ct)
{
    await Task.Delay(TimeSpan.FromSeconds(1), ct);

    // note that a service does not normally hold data
    // this example is for demonstration purposes
    return new CounterValue(++_currentCount);
}

The Feed.Async factory method can be used to create an IFeed by calling the CountOne method:

public IFeed<CounterValue> Value => Feed.Async(_myService.CountOne);

This is known as a 'pull' method, as the CountOne method is awaited while retrieving the data. To get the next counter value, the IFeed needs to be signaled to call the CountOne method again.

For the most part Task and ValueTask are interchangeable. However, with MVUX if the method returns Task<T> the method needs to be awaited in the Feed.Async callback.

// Service method
public async Task<CounterValue> CountOne() { ... }

// Feed creation - needs to await the CountOne call
public IFeed<CounterValue> CurrentCount => Feed.Async(async ct => await _myService.CountOne());

Feed.AsyncEnumerable factory

In contrast to Tasks which operate as 'pull' methods, the 'push' method is where data is returned as it becomes available via an IAsyncEnumerable instance. For example, the StartCounting method will return a new counter value every second until the CancellationToken is cancelled.

public async IAsyncEnumerable<CounterValue> StartCounting([EnumeratorCancellation] CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await Task.Delay(TimeSpan.FromSeconds(1), ct);
                
        if (ct.IsCancellationRequested)
        {
            yield break;
        }

        yield return new CounterValue(++_currentCount);
    }
}

Referring to the StartCounting method from this example, a feed can be created as follows:

public IFeed<CounterValue> CurrentCount => Feed.AsyncEnumerable(_myService.StartCounting);

CancellationTokens are essential to enable halting an ongoing async operation. However, if the API you're consuming does not have a CancellationToken parameter, you can disregard the incoming CancellationToken parameter as follows:

public IFeed<CounterValue> CurrentCount => Feed.AsyncEnumerable(ct => StartCounting());

From a ListFeed

An IListFeed<T> can be converted down to an IFeed<IImmutableCollection<T>> using the AsFeed method:

public void SetUp()
{
    IListFeed<string> stringsListFeed = ...;
    IFeed<IImmutableCollection<string>> stringsFeed = stringsListFeed.AsFeed();
}

Consumption of feeds

Awaiting feeds

Feeds are directly awaitable, so to get the data currently held in the feed, the feed needs to be awaited. This is useful when you want to use the current value in a method, for example:

public IFeed<CurrentCount> CurrentCount => ...

private async ValueTask SomeAsyncMethod()
{
    int currentCount = await CurrentCount;
}

Use feeds in an MVUX Model

The MVUX analyzers generate a bindable proxy for each of the models in your app (those with Model suffix). For the code generation to work, mark the Models and entities with the partial modifier.

For every public feed property (returning IFeed<T> or IListFeed<T>) found in the model, a corresponding property is generated on the bindable proxy.

MVUX recommends using plain POCO (Plain Old CLR Object) record types for the models in your app as they're immutable, and will not require any property change notifications to be raised. The generated bindable proxy and its properties ensure that data-binding will work, even though property change notifications aren't being raised by the models themselves.

With regular data-binding

Feeds can be consumed directly by data-binding to the feed property declared on the Model. MVUX code-generation engine ensures the feeds and all entities they expose are going to be seamlessly data-bound with the XAML data-binding engine.

The feed can be consumed directly from the View, by data binding to a property exposed on the Model:

<Page ...>
    <TextBlock Text="{Binding CurrentCount.Value}" />
</Page>

With the FeedView control

The FeedView control has been designed to work with feeds and is tailored to the additional metadata mentioned earlier that are disclosed by the feed and respond to it automatically and efficiently.

The FeedView has templates that change the visible contents based on the current state of the data, such as when the data request is still in progress, an error has occurred, or when the data contained no records.
Built-in templates are included with the FeedView for these states, but they can all be customized.

Here's how to utilize the FeedView to display the same data as before:

<Page
    ...
    xmlns:mvux="using:Uno.Extensions.Reactive.UI">
    
    <mvux:FeedView Source="{Binding CurrentCount}">
        <DataTemplate>
            <TextBlock DataContext="{Binding Data}" Text="{Binding Value}" />
        </DataTemplate>
    </mvux:FeedView>
</Page>

The FeedView wraps the data coming from the feed in a special FeedViewState class which includes the feed metadata. One of its properties is Data, which provides access to the actual data of the feed's current state, in our example the most recent integer value from the CountOne or StartCounting method above.

Feed Operators

An IFeed supports some LINQ operators that can be used to apply a transform and return a new IFeed.

Where

The Where extension method enables filtering a Feed. It returns a new Feed where the values of the parent one match the specified criteria.
For example:

public IFeed<CounterValue> OmitEarlyCounts => CurrentCount.Where(currentCount => currentCount.Value > 10);

It's worth noting that unlike IEnumerable<T>, IObservable<T>, and IAsyncEnumerable<T>, if the predicate returns false, a result is still received but it contains a Message<T> with a data Option<T> of None.

Select or SelectAsync

This one enables projecting one feed into another one by selecting one of its properties, or by passing it as a parameter to an external function.

public IFeed<int> OmitEarlyCounts => CurrentCount.Select(currentCount => currentCount.Value);

The selection can also be asynchronous, and even use an external method:

public IFeed<CountInfo> CountTrends => CurrentCount.SelectAsync(currentCount => myService.GetCountInfoAsync(currentCount));

AsListFeed

When the current feed is of IImmutableCollection<T>, you might want to consider using a list-feed. This operator lets you easily convert an IFeed<IImmutableCollection<T>> to an IListFeed<IImmutableCollection<T>>.

For example:

public void SetUp()
{
    IFeed<IImmutableCollection<string>> stringsFeed = ...;
    IListFeed<string> stringsListFeed = stringsFeed.AsListFeed();
}

LINQ syntax

You can use the LINQ syntax if you prefer, which can improve the readability of your code, particularly if you combine multiple operators:

public IFeed<int> OmitEarlyCounts =>
    from currentCount in CurrentCount
    where currentCount.Value > 10
    select currentCount.Value;