AbreVinci.Embryo.Generator 1.4.0-beta0000

This is a prerelease version of AbreVinci.Embryo.Generator.
There is a newer version of this package available.
See the version list below for details.
dotnet add package AbreVinci.Embryo.Generator --version 1.4.0-beta0000                
NuGet\Install-Package AbreVinci.Embryo.Generator -Version 1.4.0-beta0000                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AbreVinci.Embryo.Generator" Version="1.4.0-beta0000" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add AbreVinci.Embryo.Generator --version 1.4.0-beta0000                
#r "nuget: AbreVinci.Embryo.Generator, 1.4.0-beta0000"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install AbreVinci.Embryo.Generator as a Cake Addin
#addin nuget:?package=AbreVinci.Embryo.Generator&version=1.4.0-beta0000&prerelease

// Install AbreVinci.Embryo.Generator as a Cake Tool
#tool nuget:?package=AbreVinci.Embryo.Generator&version=1.4.0-beta0000&prerelease                

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() {}
}

using ActionContext = ActionContext<State, Command>;

public static class Reducer
{
	public static ActionContext Init() => new ActionContext(new State(0, true, false)).WithCommand(new Command.Load()),

	public static ActionContext Update(this ActionContext context, Message message) =>
		message switch
		{
			Message.Increment => context.Increment(),
			Message.SetCounter(var value) => context.SetCounter(value),
			Message.LoadComplete(var value) => context.LoadComplete(value),
			Message.Save => context.Save(),
			Message.SaveComplete => context.SaveComplete(),
			_ => context
		};
}

// 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 ActionContext Increment(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = state.Counter + 1
		};
		return context.WithState(newState);
	}

	public static ActionContext SetCounter(this ActionContext context, int value)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = value
		};
		return context.WithState(newState);
	}

	public static ActionContext LoadComplete(this ActionContext context, int value)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = value,
			IsLoading = false
		};
		return context.WithState(newState);
	}

	public static ActionContext Save(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			IsSaving = true
		};
		return context.WithState(newState).WithCommand(new Command.Save());
	}

	public static ActionContext SaveComplete(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			IsSaving = false
		};
		return context.WithState(newState);
	}
}

If you want to reduce the boilerplate and take advantage of some performance boosts, you can use AbreVinci.Embryo.Generator in order to automatically generate message types and reducer update from action functions.

You do this by adding a GenerateMessageType attribute to the message type and making it partial as well as mark up all actions with an Action attribute. This will generate the same message types as in the previous example but with support for more optimized switching in the reducer which you can generate the update function for using the GenerateReducerUpdate attribute.

The generated code can implementation style can still be used manually, but it's a lot more tedious and hard to read than the cleaner manual implementation from before.

// automatic generation of message types and reducer with AbreVinci.Embryo.Generator

using AbreVinci.Embryo.Generator.Attributes;

[GenerateMessageType]
public abstract partial record Message;

using ActionContext = ActionContext<State, Command>;

[GenerateReducerUpdate(typeof(State), typeof(Message), typeof(Command))]
public static partial class Reducer
{
	public static ActionContext Init() => new ActionContext(
		new State(0, true, false))
		.WithCommand(new Command.Load());
}

public static class Actions
{
	[Action]
	public static ActionContext Increment(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = state.Counter + 1
		};
		return context.WithState(newState);
	}

	[Action]
	public static ActionContext SetCounter(this ActionContext context, int value)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = value
		};
		return context.WithState(newState);
	}

	[Action]
	public static ActionContext LoadComplete(this ActionContext context, int value)
	{
		var state = context.State;
		var newState = state with
		{
			Counter = value,
			IsLoading = false
		};
		return context.WithState(newState);
	}

	[Action]
	public static ActionContext Save(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			IsSaving = true
		};
		return context.WithState(newState).WithCommand(new Command.Save());
	}

	[Action]
	public static ActionContext SaveComplete(this ActionContext context)
	{
		var state = context.State;
		var newState = state with
		{
			IsSaving = false
		};
		return context.WithState(newState);
	}
}

// The generated message type looks like this (you can always navigate to it in your IDE by going to the "other partial part" of your message type):
public abstract partial record Message
{
	public const int _IncrementTag = 1;
	public const int _SetCounterTag = 2;
	public const int _LoadCompleteTag = 3;
	public const int _SaveTag = 4;
	public const int _SaveCompleteTag = 5;

	// Actions
	public sealed record Increment() : Message(_IncrementTag);
	public sealed record SetCounter(int Value) : Message(_SetCounterTag);
	public sealed record LoadComplete(int Value) : Message(_LoadCompleteTag);
	public sealed record Save() : Message(_SaveTag);
	public sealed record SaveComplete() : Message(_SaveCompleteTag);

	// Prevent external inheritance
	private Message(int _tag)
	{
		_Tag = _tag;
	}

	public int _Tag { get; }
}

// And the generated reducer code looks like this (you can always navigate to it in your IDE by going to the "other partial part" of your reducer type):
public static partial class Reducer
{
	// This compiles to a regular switch statement which executes much faster than the type switching reducer implementation from before.
	public static ActionContext<State, Command> Update(this ActionContext<State, Command> context, Message message)
	{
		return message._Tag switch
		{
			Message._IncrementTag => context.Increment(),
			Message._SetCounterTag when (Message.SetCounter)message is var m => context.SetCounter(m.Value),
			Message._LoadCompleteTag when (Message.LoadComplete)message is var m => context.LoadComplete(m.Value),
			Message._SaveTag => context.Save(),
			Message._SaveCompleteTag => context.SaveComplete(),
			_ => context
		};
	}
}

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 AbreVinci.Embryo.EffectContext for this.

using System;
using System.Threading.Tasks;
using AbreVinci.Embryo;

// 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;
		var context = new EffectContext<Message>(dispatch);
		_subscription = commands.Subscribe(async command =>
		{
			await ExecuteAsync(command, context);
		});
    }

	private Task ExecuteAsync(Command command, EffectContext<Message> dispatch) =>
		command switch
		{
			Command.Load => context.ExecuteAsync(() => _persistanceService.LoadAsync(), createOnCompletionMessage: value => new Message.LoadComplete(value)),
			Command.Save(var value) => context.ExecuteAsync(() => _persistanceService.SaveAsync(value), createOnCompletionMessage: () => 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();
}

Just like you could generate message types and reducer from actions, you can generate command types and command handler from effects marked with an Effect attribute. For this, you need the AbreVinci.Embryo.Generator package and you need to decorate your command type with GenerateCommandType attribute and make it empty and partial. For the command handler you need a partial class decorated with the GenerateCommandHandler attribute.

// automatic generation of command types and command handler with AbreVinci.Embryo.Generator

using AbreVinci.Embryo.Generator.Attributes;

[GenerateCommandType]
public abstract partial record Command;

[GenerateCommandHandler(typeof(Command), typeof(Message))]
public partial class CommandHandler
{
}

using PersistanceContext = PersistanceContext<State, Command>;

public static class Effects
{
	[Effect]
	public static Task Load(PersistanceContext context)
	{
		return context.ExecuteAsync(
			service => service.LoadAsync(),
			createOnCompletionMessage: value => new Message.LoadComplete(value));
	}

	[Effect]
	public static Task Save(PersistanceContext context, int value)
	{
		return context.ExecuteAsync(
			service => service.SaveAsync(value),
			createOnCompletionMessage: () => new Message.SaveComplete());
	}
}

// The generated command type looks like this (you can always navigate to it in your IDE by going to the "other partial part" of your command type):
public abstract partial record Command
{
	public const int _LoadTag = 1;
	public const int _SaveTag = 2;

	// Effects
	public sealed record Load() : Command(_LoadTag);
	public sealed record Save() : Command(_SaveTag);

	// Prevent external inheritance
	private Command(int _tag)
	{
		_Tag = _tag;
	}

	public int _Tag { get; }
}

// And the generated command handler code looks like this (you can always navigate to it in your IDE by going to the "other partial part" of your command handler type):
public partial class CommandHandler : IDisposable
{
	private readonly IDisposable _subscription;

	public CommandHandler(
		IObservable<Command> commands,
		Action<Message> dispatch,
		IPersistanceService persistanceService)
	{
		Context = new EffectContext<Message>(dispatch);
		PersistanceServiceContext = new EffectContext<IPersistanceService, Message>(persistanceService, dispatch);

		_subscription = commands.Subscribe(async command =>
		{
			await ExecuteAsync(command, context);
		});
    }

	protected EffectContext<Message> Context { get; }
	protected EffectContext<IPersistanceService, Message> PersistanceServiceContext { get; }

	protected virtual Task ExecuteAsync(Command command, EffectContext<Message> dispatch)
	{
		// This compiles to a regular switch statement which executes much faster than the type switching command handler implementation from before.
		return command._Tag switch
		{
			Command._LoadTag => Effects.Load(PersistanceServiceContext),
			Command._SaveTag when (Command.Save)command is var c => Effects.Save(PersistanceServiceContext, c.Value),
			_ => Task.CompletedTask
		};
	}

	protected Task WrapAsAsync(Action action)
	{
		action();
		return Task.CompletedTask;
	}

	public void Dispose()
    {
		_subscription.Dispose();
    }
}

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 Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.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.

Version Downloads Last updated
1.7.0 879 9/2/2022
1.6.0 636 2/8/2022
1.5.0 415 1/30/2022
1.4.1 428 1/22/2022
1.4.0 426 1/22/2022
1.4.0-beta0000 192 1/22/2022