Skip to content

Commit

Permalink
Merge pull request #879 from unoplatform/dev/dr/docsModel
Browse files Browse the repository at this point in the history
docs(feed): Doc for 2.3 release
  • Loading branch information
dr1rrb authored Oct 31, 2022
2 parents 358d7a9 + 456e7f9 commit 97f9cca
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 19 deletions.
82 changes: 67 additions & 15 deletions doc/Overview/Reactive/in-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ uid: Overview.Reactive.InApps
---
# Usage in applications of feeds

The recommended use for feeds is only in view models.
The recommended use for feeds is only in models.

The reactive framework allows you to design state-less view models, focusing on the presentation logic.
Your view models only have to request in their constructors the user _ inputs_ that are expected from the view. Inputs could be:
* An `IInput<T>` to get _data_ from the view (e.g. for 2-way bindings);
* An `ICommandBuilder` for the "trigger" inputs.

In order to easily interact with the binding engine in a performant way, a `BindableXXX` class is automatically generated. It is this class that will hold the state and which **has to be set as `DataContext` of your page**.
The Reactive framework allows you to design state-less _models_, focusing on the presentation logic.
They are expected to expose some `IFeed`, `IListFeed`, `IState` or `IListState`.
A bindable friendly and higly performant _view model_ is then automatically generated by the Reactive framework.
It is this class that will hold the state and which **has to be set as `DataContext` of your page**.

> [!NOTE]
> The bindable counterpart of a class will be created when the class name ends with "ViewModel".
> You can customize that behavior using the `[ReactiveBindable]` attribute.
> The _ViewModel_ of a _model_ is created when the class name matches the regex "Model$".
> You can customize that behavior using the `[ImplicitBindables("MyApp\.Presentation\.\.*Model$")]` on the assembly
> or the `[ReactiveBindable]` attribute on you model itself.
> [!NOTE]
> For easier View Model creation, public properties will also be accessible on the `BindableXXX` class.
> You can also access the view model itself through the `Model` property.
> To ease _models_ declaration, public properties are also accessible on the _view model_ class.
> You can also access the model itself through the `Model` property.
## Display some data in the UI (VM to View)
## Display some data in the UI (Model to View)

To render a _feed_ in the UI, you only have to expose it through a public property:
```csharp
Expand Down Expand Up @@ -131,11 +130,64 @@ so you can enhance the UX of the pagination, like display a loading indicator wh
</uer:FeedView>
```

## Selection

The reactive framework has embedded support of selected item of a `ListFeed` data-bound to a `Selector`, like the `ListView`.
It is not required to data-bind (and synchronize) the `ListView.SelectedItem`, it will instead be automatically pushed from and to the view.

```xml
<uer:FeedView Source="{Binding Items}">
<DataTemplate>
<ListView ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
```

And in the model:

```csharp
public IListState<int> Items => ListView
.Value(this, () => ImmutableList.Create(1, 2, 3, 4, 5))
.Selection(SelectedItem);

public IState<int> SelectedItem => State.Value(this, () => 3);
```

### Project selected item into another entity

It is possible to synchronize the selected item _key_ into another aggregate root object, for instance, assuming `Profile` and `Country` records:

```csharp
public record Profile(string? FirstName, string? CountryId);

public record Country(string Id, string Name);
```

The selected item automatically stored into the `Profile.CountyId` by doing:
```csharp
public IState<Profile> Profile => State.Async(this, _svc.GetProfile);

public IListState<Country> Countries => ListState
.Async(this, _svc.GetCountries)
.Selection(Profile, p => p.CountryId);
```

```xml
<uer:FeedView Source="{Binding Countries}">
<DataTemplate>
<ComboBox ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
```

> [!NOTE]
> As the `IState<Porfile> Profile` state might be `None` when an item is being selected, the `Profile` class
> must have either a parameter-less contructor, either a constructor that accepts only nullable parameters.
## Commands
The generated bindable counterpart of a class will automatically re-expose as `ICommand` public methods that are compatible (cf. "general rules" below).
The generated _view model_ of a _model_ will automatically re-expose as `ICommand` public methods that are compatible (cf. "general rules" below).

For instance, in your ViewModel:
For instance, in your _model_:

```csharp
public async ValueTask Share()
Expand All @@ -152,7 +204,7 @@ This will be exposed into an `ICommand` that can be data-bound to the `Command`

By default, if the method has a parameter `T myValue` and there is a property `Feed<T> MyValue` on the same class (matching type and name), that parameter will automatically be filled from that feed.

For instance, in your ViewModel:
For instance, in your _model_:

```csharp
public IFeed<string> Message { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,9 @@ public static string Replace(this string text, char[] chars, char replacement)

return result.ToString();
}

public static string TrimEnd(this string text, string value, StringComparison comparison)
=> text.EndsWith(value, comparison)
? text.Substring(0, text.Length - value.Length)
: text;
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ private static string GetViewModelName(INamedTypeSymbol type)
var isLegacyMode = type.ContainingAssembly.FindAttribute<ImplicitBindablesAttribute>() is { Patterns.Length: 1 } config
&& config.Patterns[0] is ImplicitBindablesAttribute.LegacyPattern;

return !isLegacyMode && type.Name.IndexOf("ViewModel", StringComparison.OrdinalIgnoreCase) < 0
? $"{type.Name}ViewModel"
: $"Bindable{type.Name}";
return isLegacyMode switch
{
true => $"Bindable{type.Name}",
_ when type.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) => $"Bindable{type.Name}",
_ => $"{type.Name.TrimEnd("Model", StringComparison.OrdinalIgnoreCase)}ViewModel",
};
}

private string Generate(INamedTypeSymbol model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ public void NestedInheritingViewModel()
=> Assert.IsNotNull(GetBindable(typeof(NestedSubViewModel)));

private Type? GetBindable(Type vmType)
=> vmType.GetNestedType(vmType.Name.Contains("ViewModel") ? $"Bindable{vmType.Name}" : $"{vmType.Name}ViewModel");
=> vmType.GetNestedType(vmType.Name switch
{
{ } name when name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) => $"Bindable{vmType.Name}",
{ } name when name.EndsWith("Model", StringComparison.OrdinalIgnoreCase) => $"{name.Substring(0, name.Length - "Model".Length)}ViewModel",
{ } name => $"{name}ViewModel",
});

private Type? GetBindable(string vmType)
{
Expand Down

0 comments on commit 97f9cca

Please sign in to comment.