Dersei.Metis 1.0.0

dotnet add package Dersei.Metis --version 1.0.0
                    
NuGet\Install-Package Dersei.Metis -Version 1.0.0
                    
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="Dersei.Metis" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dersei.Metis" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Dersei.Metis" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dersei.Metis --version 1.0.0
                    
#r "nuget: Dersei.Metis, 1.0.0"
                    
#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.
#:package Dersei.Metis@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dersei.Metis&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Dersei.Metis&version=1.0.0
                    
Install as a Cake Tool

Metis

Fluent builders for WPF - an easy way to build WPF application in pure C# fluently.

Examples

Fluent.Window.Size(200, 200)
    .Content(Fluent.Label.Content("Hello world!"))
    .Show();

In this example we create a window, set its size, add a new label with "Hello world!" text and finally show the window.

Fluent.Window.Size(200, 200)
    .Content(
        Fluent.Grid.Rows(2)
            .Child(Fluent.Label
                .Extract(out var label))
            .Child(Fluent.Button
                .Content("Click me")
                .Click((_, _) => label.Content = "Clicked")
                .Row(1)
            )
    )
    .Show();

Here instead of using a label directly, we first create a grid with two rows and then put a label and a button inside. We use Extract method to get a reference to the label and then we use it in a click event of the button to change the label's content.

Fluent.Window.DataContext<BasicViewModel>().Size(200, 200)
    .Content(
        Fluent.Grid.Rows(2)
            .Child(Fluent.Label
                .BasicBinding(nameof(BasicViewModel.WelcomeText))
                .Property(l => l.Background = Brushes.GreenYellow))
            .Child(Fluent.Button
                .Content("Click me")
                .Command(nameof(BasicViewModel.ClickCommand))
                .Background(Brushes.Red)
                .Row(1)
            )
    )
    .Show();

This example shows that fluent builders support MVVM. Using DataContext method we can tell our window to use given type as its view model. Thanks to that we can use methods like BasicBinding or Command to bind properties of GUI elements to properties from the view model. We can also see here that fluent builders support settings any of the properties of the built element using Property method.

Usage

Using Metis requires:

  • changes in csproj file:
    • TargetFramework set to netX.X-windows,
    • UseWPF set to true.
  • if used in a console app, Main method has to have [STAThread] attribute applied.

Metis supports most of the default WPF controls and some of the other useful types like:

  • Application,
  • FrameworkElementFactory,
  • LinearGradientBrush and RadialGradientBrush,
  • InputBinding,
  • Style,
  • and more.

Additionally, Metis allows basic builder support for other controls, including custom ones by either using WrapBuilder class or allowing to inherit from BaseBuilder. First can be done using Wrap extension method on a type without its own builder or using WrapBuilder.Create<T>, Fluent.Wrap<T> or Fluent.CreateWrapped<T> methods.

var customControl = new CustomControl().Wrap();
var customControl = WrapBuilder.Create<CustomControl>();
var customControl = Fluent.CreateWrapped<CustomControl>();
var customControl = Fluent.Wrap(new CustomControl());

WrapBuilder makes integration of custom or not supported controls easier and allows to edit them using builder pattern:

public class CustomControl : Control
{
    public Color AccentColor { get; set; }
}

var customControl = Fluent.CreateWrapped<CustomControl>()
    .Property(c => c.AccentColor = Colors.GreenYellow)
    .ZIndex(10);

It's also possible to use existing builders with classes inheriting from supported controls:

public class CustomButton : Button 
{
    public int MaxClick { get; set; }
}

ButtonBuilder b = new CustomButton().AsFluent();

Created this way builders won't support members from the child class.

Real example

Let's create a simple tic-tac-toe app.

  1. First we create a view class.
public class TicTacToeView
{
    public void Show()
    {
        
    }
}
  1. We also want our app to use MVVM, so let's create a view model using ObservableObject from MVVM Toolkit.
public class TicTacToeViewModel : ObservableObject
{
    private int _score;
    public TicTacToeViewModel()
    {
    }

    public int Score {
        get => _score;
        set => SetProperty(ref _score, value);
    }
}


This way we can display and increase score.

  1. Obviously our app needs a way to display both score and a board. That means it should have a grid with at least two rows. Let's create a window first.
  public void Show()
  {
      Fluent.VmWindow<TicTacToeViewModel>();
  }

The window DataContext needs to be set to a proper class. It can be done in a few ways, including the one above or:

Fluent.Window.DataContext<TicTacToeViewModel>();

The result is the same, so we'll just stay with the first one. Now when we have the window, it's time to create a grid with two rows, one much smaller than the other. We'll also want our window to be higher rather than wider. Using Show() we can see the result of our code.

Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
      )
      .Show();

Right now everything we can see is a white window.

  1. Let's add a label with a bit more color.
Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
              .Child(
                  Fluent.Label
                      .Background(Brushes.Green)
                  )
      )
      .Show();
  1. The label should display our score.
Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
              .Child(
                  Fluent.Label
                      .Background(Brushes.Green)
                      .BasicBinding(nameof(TicTacToeViewModel.Score))
                  )
      )
      .Show();

Some of the builders allow setting a basic binding - the one that's the most popular for a specific control. What we see now doesn't look particularly well, let's fix that a bit.

Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
              .Child(
                  Fluent.Label
                      .Background(Brushes.Green)
                      .Foreground(Brushes.White)
                      .BasicBinding(nameof(TicTacToeViewModel.Score))
                      .HorizontalContentAlignment(HorizontalAlignment.Center)
                      .Property(l => l.FontSize = 60)
                  )
      )
      .Show();

Here we can see two ways to set a property. Some of the properties, especially the most basic ones have their own method like Background or HorizontalContentAlignment in this case. Others can be set using generic Property method.

  1. Now we can add a grid that will serve as our board. Since it's not a first child of the parent grid, it needs to have its Row property set. There are two ways to do this. One using methods of the builder and another using a new builder that creates and sets layout properties. We'll use the second one but generally the first one should fit better in simple cases like this one.
Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
              .Child(
                  Fluent.Label
                      .Background(Brushes.Green)
                      .Foreground(Brushes.White)
                      .BasicBinding(nameof(TicTacToeViewModel.Score))
                      .HorizontalContentAlignment(HorizontalAlignment.Center)
                      .Property(l => l.FontSize = 60)
                  )
              .Child(
                  Fluent.Grid
                      .Rows("*,*,*")
                      .Columns("*,*,*")
                      .LayoutProperties(Fluent.GridProperties.Row(1))
                  )
      )
      .Show();
  1. Let's make our grid looks more like tic-tac-toe board. For that we need nine controls representing fields. We could use labels but for variety's sake let's use buttons. To make our code more readable we'll move creation of the buttons to a new method.
private static IEnumerable<ButtonBuilder> CreateGridButtons(int rows, int columns)
{
    for (int i = 0; i < rows; i++)
    {
        for (int j = 0; j < columns; j++)
        {
            yield return Fluent.Button
                .Background(Brushes.White)
                .Property(b => b.BorderBrush = Brushes.Black)
                .Property(b => b.BorderThickness = new Thickness(5))
                .Property(b => b.FontSize = 64)
                .Row(i)
                .Column(j);
        }
    }
}
Fluent.VmWindow<TicTacToeViewModel>()
      .Size(700, 600)
      .Content(
          Fluent.Grid
              .Rows("*,6*")
              .Child(
                  Fluent.Label
                      .Background(Brushes.Green)
                      .Foreground(Brushes.White)
                      .BasicBinding("Score")
                      .HorizontalContentAlignment(HorizontalAlignment.Center)
                      .Property(l => l.FontSize = 60)
                  )
              .Child(
                  Fluent.Grid
                      .Rows("*,*,*")
                      .Columns("*,*,*")
                      .LayoutProperties(Fluent.GridProperties.Row(1))
                      .Children(
                          CreateGridButtons(3, 3)
                          )
                  )
      )
      .Show();
  1. Now we need some way for our buttons to communicate with our view model. First let's change the latter. We'll need later a way to bind buttons to an object, so we can create a simple class:
public class FieldState : ObservableObject
{
    private string? _value;

    public string? Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }
}
private readonly ObservableCollection<FieldState> _boardStatus;

public TicTacToeViewModel()
{
    _boardStatus = new ObservableCollection<FieldState>();
    for (int i = 0; i < 9; i++)
    {
        _boardStatus.Add(new FieldState());
    }
}

public ObservableCollection<FieldState> BoardStatus => _boardStatus;

We also need a way to update status of a field on a button click.

public RelayCommand<(int row, int column)> SetField { get; set; }

public TicTacToeViewModel()
{
    ...
    SetField = new RelayCommand<(int row, int column)>(Update);
    ...
}

private void Update((int row, int column) field)
{
    BoardStatus[field.row * 3 + field.column].Value = "X";
}

That's how we use this RelayCommand in our buttons:

yield return Fluent.Button
              .Background(Brushes.White)
              .Property(b => b.BorderBrush = Brushes.Black)
              .Property(b => b.BorderThickness = new Thickness(5))
              .Property(b => b.FontSize = 64)
              .Row(i)
              .Column(j)
              .Command(nameof(TicTacToeViewModel.SetField), (i, j));

Now we have a problem though, we can update the state of the board, but it won't be reflected in what we see in our GUI. There are a few ways to deal with this, but we'll want to stay as much faithful to MVVM as possible.

  1. Instead of using grid for our board we'll use ItemsControl, that we'll allow us to bind buttons properties to items in BoardStatus collection.
public void Show()
{
    ...
        .Child(
            Fluent.ItemsControl
                .ItemsSource(nameof(TicTacToeViewModel.BoardStatus))
                .LayoutProperties(Fluent.GridProperties.Row(1))
        )
    ...
}

Now we have an ItemsControl with its source set. Of course right now, the results are far from perfect, so we need to set a few other properties. To set how the ItemsControl should look like we can use ItemPanel method. ItemPanel receives either a regular builder or FrameworkElementFactoryBuilder. In our case the latter gives us more options, so we'll use it. We can either create it manually or by using an extension method on an element builder. GridBuilder has its own method that also adds its columns and rows definitions.

...
  .ItemPanel(
          Fluent.Grid
              .Columns("*,*,*")
              .Rows("*,*,*")
              .ToFactory()
      )
...

After defining ItemPanel we also need to define what items inside ItemsControl should look like. We can use ItemTemplate method for that.

    ...
        .ItemPanel(
                  Fluent.Grid
                      .Columns("*,*,*")
                      .Rows("*,*,*")
                      .ToFactory()
        )
        .ItemTemplate(
            Fluent.Button
                .BasicBinding(nameof(FieldState.Value))
                .Background(Brushes.White)
                .Property(b => b.BorderBrush = Brushes.Black)
                .Property(b => b.BorderThickness = new Thickness(5))
                .Property(b => b.FontSize = 64)
                .ToFactory()
        )
    ...

Furthermore, we also need our buttons to be in correct row and column. For that we need to support this in our model:

public class FieldState : ObservableObject
{
    private string? _value;
    private int _column;
    private int _row;

    public string? Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }
    
    public int Column
    {
        get => _column;
        set => SetProperty(ref _column, value);
    }

    public int Row
    {
        get => _row;
        set => SetProperty(ref _row, value);
    }
}
public TicTacToeViewModel()
{
    SetField = new RelayCommand<(int row, int column)>(Update);
    _boardStatus = new ObservableCollection<FieldState>();
    for (int i = 0; i < 9; i++)
    {
        var row = i / 3;
        var column = i % 3;
        _boardStatus.Add(new FieldState
        {
            Row = row,
            Column = column
        });
    }
}

Now we need to use ItemContainerStyle of ItemsControl. That method accepts StyleBuilder, so we can use it here.

    ...
        .ItemContainerStyle(
              Fluent.Style
                  .Binding(Grid.RowProperty,
                      nameof(FieldState.Row))
                  .Binding(Grid.ColumnProperty,
                      nameof(FieldState.Column))
              )
    ...

Finally, we can see our buttons set correctly in a grid.

  1. We can delete our CreateGridButtons method. We still need a way to send button position to the view model. Let's do this in a simple way. We need to change both our model and our view model again.
public class FieldState : ObservableObject
{
    private string? _value;
    private int _column;
    private int _row;
    private int _index;

    public string? Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }
    
    public int Column
    {
        get => _column;
        set => SetProperty(ref _column, value);
    }

    public int Row
    {
        get => _row;
        set => SetProperty(ref _row, value);
    }
    
    public int Index
    {
        get => _index;
        set => SetProperty(ref _index, value);
    }
}
    ...
        _boardStatus.Add(new FieldState
        {
            Row = row,
            Column = column,
            Index = i
        });
    ...
      public RelayCommand<int> SetField { get; set; }
      ...
      SetField = new RelayCommand<int>(Update);
      ...
      private void Update(int index)
      {
          BoardStatus[index].Value = "X";
      }

Now we can use FieldState.Index for a command parameter like this:

    ...
        .ItemTemplate(
            Fluent.Button
                .BasicBinding(nameof(FieldState.Value))
                .Background(Brushes.White)
                .Property(b => b.BorderBrush = Brushes.Black)
                .Property(b => b.BorderThickness = new Thickness(5))
                .Property(b => b.FontSize = 64)
                .Binding(ButtonBase.CommandProperty,
                    new Binding(nameof(MinesweeperViewModel.ClickCommand))
                        .UseWindowDataContext())
                .Binding(ButtonBase.CommandParameterProperty,
                   nameof(FieldState.Index))
                .ToFactory()
        )
    ...

In contrast to other binding we've set so far this time we need to use new Binding object, since thanks to that we can use UseWindowDataContext extension method. That way we can bind our buttons' Command to the property in the view model and not FieldState class. We don't need to do this for CommandParameter though, because in that case we want to use the model's property. We can now run our app, and we'll see a board with working buttons.

  1. Let's add a very basic opponent to our app. We need a method in our view model that will select a field that hasn't been set yet and change its value to "O":
    private void SetRandomField()
    {
        var list = new List<FieldState>();
        
        foreach (var fieldState in BoardStatus)
        {
            if (fieldState.Value is null)
            {
                list.Add(fieldState);
            }
        }
        
        if(list.Count > 0)
            list[Random.Shared.Next(0, list.Count)].Value = "O";
    }

Now let's call it in our Update method.

    private void Update(int index)
    {
        BoardStatus[index].Value = "X";
        SetRandomField();
    }

And now we have a very primitive tic-tac-toe.

  1. Currently, it's possible to click every button even after its value is set. Let's change that with a simple property in FieldState.
    public bool IsNotSet => _value is null;

We also need to inform our view about change:

    public string? Value {
        get => _value;
        set
        {
            SetProperty(ref _value, value);
            OnPropertyChanged(nameof(IsNotSet));
        }
    }

And now we'll add a proper binding to our buttons:

          Fluent.Button
              .BasicBinding(nameof(FieldState.Value))
              .Background(Brushes.White)
              .Property(b => b.BorderBrush = Brushes.Black)
              .Property(b => b.BorderThickness = new Thickness(5))
              .Property(b => b.FontSize = 64)
              .Binding(ButtonBase.CommandProperty,
                  new Binding(nameof(TicTacToeViewModel.SetField))
                      .UseWindowDataContext())
              .Binding(ButtonBase.CommandParameterProperty,
                 nameof(FieldState.Index))
              .Binding(b => b.IsEnabled, 
                  nameof(FieldState.IsNotSet))
              .ToFactory()
      )
  1. Our GUI works, time to implement winning. There are many ways to do this, so we can choose one of it. For this tutorial the most important part is scoring and ending or resetting the game. Let's say that every win for player is Score++, and every win for "the opponent" is Score--. It would be nice to show player a result and ask if they want to play a new game. We can use MessageBox to do this.

  2. We won't complicate things here. We need to change our upper row a bit and add to our score label a button.

    ...
      .Rows("*,6*")
      .Child(
          Fluent.Grid
              .Columns("5*,*")
              .Child(
                  Fluent.Label
                      .Column(0, 2)
                      .Background(Brushes.Green)
                      .Foreground(Brushes.White)
                      .BasicBinding(nameof(TicTacToeViewModel.Score))
                      .HorizontalContentAlignment(HorizontalAlignment.Center)
                      .Property(l => l.FontSize = 60)
              )
              .Child(
                  Fluent.Button
                      .Column(1)
                      .Content("New game")
              )
      )
    ...

We use Column method with span so Label remains centered. Let's add a new method to the view model and a new binding to our button.

public RelayCommand NewGame { get; set; }
  ...
    SetField = new RelayCommand<int>(Update);
    NewGame = new RelayCommand(Reset);
    _boardStatus = new ObservableCollection<FieldState>();
  ...
    private void Reset()
    {
        foreach (var state in _boardStatus)
        {
            state.Value = null;
        }
    }

And in our view:

    ...
        Fluent.Button
            .Column(1)
            .Content("New game")
            .Command(nameof(TicTacToeViewModel.NewGame))
    ...
  1. Now it's possible to reset or start a game anytime. We should probably disable a board after the win. To do this we need a new property and a new binding.
private bool _isEnabled = true; 
public bool IsEnabled {
    get => _isEnabled;
    set => SetProperty(ref _isEnabled, value);
}

We need to set this property to false when the game ends. And to true in Reset method.

    ...
        .ItemPanel(
        Fluent.Grid
          .Columns("*,*,*")
          .Rows("*,*,*")
          .Binding(UIElement.IsEnabledProperty, nameof(TicTacToeViewModel.IsEnabled))
          .ToFactory()
        )
    ...

And that way we made a working tic-tac-toe game (kind of), it's not the best and definitely not a challenge, but it works.

Name

Metis - in ancient Greek religion and mythology, was the pre-Olympian goddess of wisdom, counsel and deep thought, and a member of the Oceanids.

Metis - Wikipedia.

Product Compatible and additional computed target framework versions.
.NET net8.0-windows7.0 is compatible.  net9.0-windows was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0-windows7.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.0.0 150 6/23/2025