Components are reusable UI elements, Compose
build components around composable methods. Through these methods, Programmatically define the application's UI. To create composable method, Simply create an object that inherits IComposable
to implement the Compose
method, calls to the Compose
method trigger re-rendering of the component, if require component parameters prefix overload modifier, if inherited prefix virtual modifier. Component's root element, child element and elements that need to ensure that focus is not lost due to rendering such as TextBox
, needs to be defined in class scope and guaranteed read-only. All other elements can be defined in methods, through Configure
extension method for more easily configure elements.
In wpf, Some elements do not clear the connection with the previous parent when assigning to new parent. Forcing the assignment will result in an error. For example, the Children
property of Panel
, the Child
property of Border
. Which needs to be assigned with some extension methods, such as SetChildren
, SetChild
, which clear the connection with the previous parent element before added to new parent.
internal class MainPage : IComposable
{
// Cascade Style
private readonly StackPanel _stackPanel = new() { Resources = new ResourceDictionary { Source = new Uri("Resources/Styles.xaml", UriKind.Relative) } };
private readonly Counter _counter = new();
private int _value;
public UIElement Compose()
{
_stackPanel.SetChildren(new StackPanel().Configure(x =>
{
x.SetChildren(
new TextBlock { Text = "Hello, world!", Style = (Style)_stackPanel.Resources["Heading"]! },
new TextBlock { Text = "Welcome to your new app." },
// Child Component
_counter.Compose(_value, OnCounterValueChanged));
}));
// Condition render
for (var i = 0; i < _value; i++) _stackPanel.Children.Add(new TextBlock { Text = "Item:" + i });
return _stackPanel;
}
private void OnCounterValueChanged(int value)
{
_value = value;
Compose();
}
}
internal class Counter : IComposable
{
private readonly StackPanel _stackPanel = new();
private readonly TextBox _textBox = new();
private int _value;
private Action<int> _valueChanged = null!;
public UIElement Compose(int value, Action<int> valueChanged)
{
(_value, _valueChanged) = (value, valueChanged);
return Compose();
}
public UIElement Compose()
{
_stackPanel.SetChildren(
// Data binding
new TextBlock { Text = "Count:" + _value },
// Event binding
new Button { Content = "Add" }.Configure(x => x.Click += (_, _) => _valueChanged(++_value)),
// Two-way binding
_textBox.Configure(x =>
{
x.Text = _value.ToString();
x.SetTextChanged(y =>
{
if (int.TryParse(y, out var value)) _valueChanged(value);
});
}));
return _stackPanel;
}
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="StackPanel">
<Setter Property="Background" Value="SlateBlue"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="20"/>
</Style>
<Style x:Key="Heading" TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="FontSize" Value="30"/>
</Style>
</ResourceDictionary>
In compose, any object can be used as a page, use NavigationService
manage navigation in code. If a object inherited IComposable
, then the component generated by it's Compose
method is used as the page.
Some page might need dependency services or route values, need use constructor for injection, add the required service and routing values by adding parameters to the constructor. When navigating to the page, will provide these instances. In below examples, constructor use DI receive NavigationService
for navigation,and passes the message
routing value.
internal class BeforePage(NavigationService navigationService, string message) : IComposable
{
private readonly StackPanel _stackPanel = new();
public UIElement Compose()
{
_stackPanel.SetChildren(
new TextBlock { Text = message },
new Button { Content = "Push" }.Configure(x => x.Click += OnClick));
return _stackPanel;
}
private void OnClick(object sender, RoutedEventArgs e) => navigationService.Push<AfterPage>("Hello from BeforePage");
}
internal class AfterPage(NavigationService navigationService, string message) : IComposable
{
private readonly StackPanel _stackPanel = new();
public UIElement Compose()
{
_stackPanel.SetChildren(
new TextBlock { Text = message },
new Button { Content = "Pop" }.Configure(x => x.Click += OnClick));
return _stackPanel;
}
private void OnClick(object sender, RoutedEventArgs e) => navigationService.Pop();
}
public partial class MainWindow
{
public MainWindow(NavigationService navigationService)
{
InitializeComponent();
//Content = new MainPage().Compose();
Loaded += (_, _) => navigationService.Push<BeforePage>("Hello from MainWindow");
}
}