Dersei.Metis
1.0.0
dotnet add package Dersei.Metis --version 1.0.0
NuGet\Install-Package Dersei.Metis -Version 1.0.0
<PackageReference Include="Dersei.Metis" Version="1.0.0" />
<PackageVersion Include="Dersei.Metis" Version="1.0.0" />
<PackageReference Include="Dersei.Metis" />
paket add Dersei.Metis --version 1.0.0
#r "nuget: Dersei.Metis, 1.0.0"
#:package Dersei.Metis@1.0.0
#addin nuget:?package=Dersei.Metis&version=1.0.0
#tool nuget:?package=Dersei.Metis&version=1.0.0
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 tonetX.X-windows
,UseWPF
set totrue
.
- 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
andRadialGradientBrush
,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.
- First we create a view class.
public class TicTacToeView
{
public void Show()
{
}
}
- 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.
- 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.
- 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();
- 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.
- 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();
- 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();
- 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.
- Instead of using grid for our board we'll use
ItemsControl
, that we'll allow us to bind buttons properties to items inBoardStatus
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.
- 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.
- 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.
- 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()
)
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" isScore--
. It would be nice to show player a result and ask if they want to play a new game. We can useMessageBox
to do this.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))
...
- 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.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
-
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 |