AbreVinci.Embryo.Entities
1.0.0
See the version list below for details.
dotnet add package AbreVinci.Embryo.Entities --version 1.0.0
NuGet\Install-Package AbreVinci.Embryo.Entities -Version 1.0.0
<PackageReference Include="AbreVinci.Embryo.Entities" Version="1.0.0" />
paket add AbreVinci.Embryo.Entities --version 1.0.0
#r "nuget: AbreVinci.Embryo.Entities, 1.0.0"
// Install AbreVinci.Embryo.Entities as a Cake Addin #addin nuget:?package=AbreVinci.Embryo.Entities&version=1.0.0 // Install AbreVinci.Embryo.Entities as a Cake Tool #tool nuget:?package=AbreVinci.Embryo.Entities&version=1.0.0
AbreVinci.Embryo
Introducing MVVMU (model, view, view model, update). This is a library and a framework for .NET UI development. It is primarily designed to be used with MVVM technologies such as WPF and Xamarin. It acts as a framework and adapter that allows you to write your applications using functional code and modern C#.
Note:
AbreVinci.Embryo is currently in early development and has so far only been tested in WPF applications, although theoretically it should be usable on other XAML-based MVVM technologies such as Xamarin. If you would like to use it in one of your Xamarin apps, you can try it out and report any issues that you find through the issue tracker.
Who is it for?
There are several reasons you might want to consider using Embryo for your next WPF or Xamarin project and there are a few situations when you are better off using more traditional MVVM frameworks.
Use Embryo if:
- you are creating an app of at least about 5 view models
- you are prepared to work with C# in a functional way using immutable data structures and pure functions
- you want to avoid INotifiPropertyChanged management
- you want to have complete understanding of the data flow inside your application
- you want full testability of any and all business logic and barely need any mocks or stubs for unit tests
- you don't want you application's complexity to grow much when you add features
- you want an extremely loosely coupled architecture
- you are curious about trying the MVU support inside MAUI in which case you can reuse a lot of your application
Don't use Embryo if:
- you do not want to work with functional or reactive code
- your project is small or trivial
- your project is not a WPF or Xamarin project
- you don't want a framework that "tells you how to do things"
Introduction & Motivation
MVVM and MVU are two different architectural patterns to create applications.
MVVM
If you want a quick intro to MVVM you might want to check here before you continue reading.
When it comes to MVVM, it may be worth noting that there are an immense amount of MVVM frameworks available and different code bases are using the pattern in a vast amount of different ways. So MVVM as a pattern is pretty and can fit many different styles.
When it comes to views and data binding, most MVVM implementations are very similar. But when looking at how the view models are implemented, what change notification mechanism is being used, how they communicate with the model and with each other, and what model actually means, there is no one answer. The result is usually a code base that grows exponentially in complexity when a product is developed.
In some ways, this is a risk of using MVVM as a pattern. The fact that it is not well defined makes it harder to onboard new team members and decide where things belong in the code base.
MVU
MVU, sometimes called the Elm architecture, is a pattern inspired by functional programming and allows for and takes a more holistic approach to the architecture of the entire application towards a more functional approach.
It is not widely used within WPF or Xamarin, although there is one project that can be used from F# called Elmish.
However, it is widely used within web development, primarily going under the name Redux.
Pure MVU renders the view by computing a data structure representation of the view that is then compared to what was previously show on screen and updated elements are re-rendered.
WPF and xamarin do not lend themselves well to this form of rendering. Therefore the WPF version of Elmish generates a view model instead of a view in the rendering function, declaring bindable properties.
Embryo (MVVMU)
Embryo is a reactive hybrid between MVVM and MVU, and we call it MVVMU.
Embryo does something similar to what Elmish.WPF does with keeping view models as the bindable entities but it uses ReactiveX to drive the bindings and change notifications under the hood. You just have to declare what you want the view models to expose to the views and you're done. No manual management of change notifications.
But when it comes to the non-view part of MVU, it is essentially a huge state machine with an immutable application state and an update function to transition from one state to another.
The Embryo Architecture (MVVMU)
An overview of the Embryo architecture can seen viewed here.
The 'Program'
In Embryo, you store your application runtime state in a state machine which is implemented by the AbreVinci.Embryo.Program<TState, TMessage, TCommand>
class and relies on an update function implemented by you. It is the only source of truth for the application state. It makes use of ReactiveX and exposes four members:
void DispatchMessage(TMessage message)
(causes update to run in order to compute a new state and dispatch commands)IObservable<TState> States { get; }
(emits the latest state instance after every update)IObservable<TCommand> Commands { get; }
(emits any commands that are returned from update)IObservable<TMessage> Messages { get; }
(emits dispatched messages)
These 4 members are at the heart of your application and are normally the only thing that tie different parts of the application together. It might tke a bit of getting used to the concept of a single big central state machine driving the application, but it greatly reduces interdependencies and couplings overall.
The Reducer (init and update functions)
In order to create an instance of AbreVinci.Embryo.Program<TState, TMessage, TCommand>
you need to supply an init and an update function. Because their usage is similar to that of the functional reduce operation on a list, they are often members of a so called reducer. They must have the signatures:
(TState, IImmutableList<TCommand>) Init()
and
(TState, IImmutableList<TCommand>) Update(TState state, TMessage message)
TState
may refer to any immutable and serializable state type capable of holding your application state. Using record types from C# 9 is a great option.
TMessage
should also be immutable and serializable. The ideal data type for this is usually a discriminated union, but this does not yet exist in the C# language. But it can be simulated for our purposes using records like this:
// simulating discriminated union type for Message
public abstract record Message
{
public record Increment(int ByValue) : Message;
public record Decrement(int ByValue) : Message;
// prevent inheritance outside this class (to keep all messages in the same place)
private Message() {}
}
TCommand
is like message best represented using a discriminated union style type simulated with records.
Update
Your update function receives the current application state as well as the dispatched message as arguments and needs to return a new state. You are not allowed to modify the state that you receive. Any side effects that need to occur as a result of the message can be handled by yielding one or more commands as a second return value and those can then be listened to from outside the state machine.
ViewModels
Embryo view models can be seen as adapters and they should not contain any logic (calculations, branches etc). Embryo view models select what state is mapped to what property and the framework raises appropriate property change notifications for you. They also have the possibility to dispatch messages from mutable properties and commands. The message dispatch will cause the program to update the state.
Example
Here is an example implementation of a persistant counter. For the completed implementation, see Samples/Counter.
We start off with the state, message and command types as well as the reducer as these are at the core of the application.
// functional core
// usually no to very few external dependencies and easily testable
public record State(int Counter, bool IsLoading, bool IsSaving);
public abstract record Message
{
public record Increment : Message;
public record SetCounter(int Value) : Message;
public record LoadComplete : Message;
public record Save : Message;
public record SaveComplete : Message;
private Message() {}
}
public abstract record Command
{
public record Load : Command;
public record Save : Command;
private Command() {}
}
public static class Reducer
{
public static (State, IImmutableList<Command>) Init() => new State(0, true, false).QueueCommand(new Command.Load()),
public static (State, IImmutableList<Command>) Update(State state, Message message) =>
message switch
{
Message.Increment => state.Increment().WithNoCommands(),
Message.SetCounter(var value) => state.SetCounter(value).WithNoCommands(),
Message.LoadComplete(var value) => state.LoadComplete(value).WithNoCommands(),
Message.Save => state.Save().QueueCommand(),
Message.SaveComplete => state.SaveComplete().WithNoCommands(),
_ => state.WithNoCommands()
};
}
// it is considered best practice to extract the implementation of your message handlers to methods of the same name inside
// action classes, preferably even making them extension methods for even cleaner update function:
public static class Actions
{
public static State Increment(this State state)
{
var newState = state with
{
Counter = state.Counter + 1
};
return newState;
}
public static State SetCounter(this State state, int value)
{
var newState = state with
{
Counter = value
};
return newState;
}
public static State LoadComplete(this State state, int value)
{
var newState = state with
{
Counter = value,
IsLoading = false
};
return newState;
}
public static (State, Command) Save(this State state)
{
var newState = state with
{
IsSaving = true
};
return newState.WithCommand(new Command.Save());
}
public static State SaveComplete(this State state)
{
var newState = state with
{
IsSaving = false;
};
return newState;
}
}
// adapt and include these extension methods to keep your reducer and actions clean:
public static class CommandUtilityExtensions
{
public static (TState, Command?) WithOptionalCommand<TState>(this TState state, Command? command) => (state, command);
public static (TState, Command) WithCommand<TState>(this TState state, Command command) => (state, command);
public static (TState, IImmutableList<Command>) WithCommands<TState>(this TState state, params Command[] commands) => (state, ImmutableArray.Create(commands));
public static (TState, IImmutableList<Command>) WithNoCommands<TState>(this TState state) => (state, ImmutableArray<Command>.Empty);
public static (TState, IImmutableList<Command>) QueueCommand<TState>(this (TState, Command?) pair) => (pair.Item1, pair.Item2 != null ? ImmutableArray.Create(pair.Item2) : ImmutableArray<Command>.Empty);
public static (TState, IImmutableList<Command>) QueueCommand<TState>(this TState state, Command? command) => (state, command != null ? ImmutableArray.Create(command) : ImmutableArray<Command>.Empty);
public static (TState, IImmutableList<Command>) QueueCommands<TState>(this (TState, IImmutableList<Command>) pair) => pair;
}
You can the implement your view model in this way:
// view model if not using AbreVinci.Embryo.Fody
using AbreVinci.Embryo;
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;
public class CounterViewModel : ReactiveViewModel
{
private readonly IReactiveTwoWayBinding<int> _counter;
private readonly IReactiveOneWayBinding<Visibility> _loadIndicatorVisibility;
private readonly IReactiveOneWayBinding<Visibility> _saveIndicatorVisibility;
public CounterViewModel(IObservable<State> state, Action<Message> dispatch)
{
_counter = CreateTwoWayBinding(state.Select(s => s.Counter), c => dispatch(new Message.SetCounter(c)), nameof(Counter));
_loadIndicatorVisibility = CreateOneWayBinding(state.Select(s => s.IsLoading ? Visibility.Visible : Visibility.Collapsed), nameof(LoadIndicatorVisibility));
_saveIndicatorVisibility = CreateOneWayBinding(state.Select(s => s.IsSaving ? Visibility.Visible : Visibility.Collapsed), nameof(LoadIndicatorVisibility));
Increment = BindCommand(() => dispatch(new Message.Increment()));
Save = BindCommand(() => dispatch(new Message.Save()), state.Select(s => !s.IsSaving));
}
public int Counter { get => _counter.Value; set => _counter.Value = value; }
public Visibility LoadIndicatorVisibility => _loadIndicatorVisibility.Value;
public Visibility SaveIndicatorVisibility => _saveIndicatorVisibility.Value;
public ICommand Increment { get; }
public ICommand Save { get; }
}
If you want to clean up your view model further, you may do this by installing the AbreVinci.Embryo.Fody package. This allows you to
use BindOneWay
instead of CreateOneWayBinding
and BindTwoWay
instead of CreateTwoWayBinding
and so on.
Calling those would otherwise generate exceptions at runtime as they are simply place holders for the Fody weaver and can't work without it.
// view model when using AbreVinci.Embryo.Fody, the Fody weaver will convert it to the previous non-fody example.
using AbreVinci.Embryo;
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Input;
public class CounterViewModel : ReactiveViewModel
{
public CounterViewModel(IObservable<State> state, Action<Message> dispatch)
{
Counter = BindTwoWay(state.Select(s => s.Counter), c => dispatch(new Message.SetCounter(c)));
LoadIndicatorVisibility = BindOneWay(state.Select(s => s.IsLoading ? Visibility.Visible : Visibility.Collapsed));
SaveIndicatorVisibility = BindOneWay(state.Select(s => s.IsSaving ? Visibility.Visible : Visibility.Collapsed));
Increment = BindCommand(() => dispatch(new Message.Increment()));
Save = BindCommand(() => dispatch(new Message.Save()), state.Select(s => !s.IsSaving));
}
public int Counter { get; set; }
public Visibility LoadIndicatorVisibility { get; }
public Visibility SaveIndicatorVisibility { get; }
public ICommand Increment { get; }
public ICommand Save { get; }
}
The view for this is a simple data template inside a xaml resource dictionary:
<DataTemplate x:Key="CounterView" DataType="{x:Type vm:CounterViewModel}">
<Grid Margin="20" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Text="{Binding Counter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Grid.Column="0" Grid.Row="0" />
<Button Command="{Binding Increment}" Content="Increment" Grid.Column="1" Grid.Row="0" />
<Button Command="{Binding Save}" Content="Save" Grid.Column="0" Grid.Row="1" />
<TextBlock Visibility="{Binding SaveIndicatorVisibility}" Text="Saving..." Grid.Column="1" Grid.Row="1" />
<Border Visibility="{Binding LoadIndicatorVisibility}" Background="White" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Grid.RowSpan="2">
<TextBlock Text="Loading..." HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</DataTemplate>
In this example, we use a command handler class to deal with the emitted commands and respond by dispatching messages. We use an utility static class
for this AbreVinci.Embryo.CommandHandlerUtils<TMessage>
.
using System;
using System.Threading.Tasks;
using static AbreVinci.Embryo.CommandHandlerUtils<Message>; // for ExecuteAsyncActionAsync and friends
// command handler, acts as an adapter between the program and dumb services.
// How you write this is entirely up to you and it doesn't even have to exist in all cases.
public class CommandHandler : IDisposable
{
private readonly IPersistanceService _persistanceService;
private readonly IDisposable _subscription;
public CommandHandler(IObservable<Command> commands, Action<Message> dispatch, IPersistanceService persistanceService)
{
_persistanceService = persistanceService;
_subscription = commands.Subscribe(command => ExecuteAsync(command, dispatch));
}
private Task ExecuteAsync(Command command, Action<Message> dispatch) =>
command switch
{
Command.Load => ExecuteAsyncActionAsync(_persistanceService.LoadAsync, dispatch, createSuccessMessage: value => new Message.LoadComplete(value)),
Command.Save => ExecuteAsyncActionAsync(_persistanceService.SaveAsync, dispatch, createSuccessMessage: () => new Message.SaveComplete()),
_ => throw new NotImplementedException()
};
public void Dispose()
{
_subscription.Dispose();
}
}
// The persistance service interface that would have to be implemented in this case.
public interface IPersistanceService
{
Task<int> LoadAsync();
Task SaveAsync();
}
If you want to check out a more advanced sample, you might want to look at the todo list sample in Samples/Todoist. It is usually first when used in a bigger application that this architecture can be truly benefitted from. Setting up the application from the start might take a bit of effort but it scales much better.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.