Best Practices

Guidelines for writing maintainable, performant PlusUi applications.


MVVM Pattern

Do: Separate Concerns

// ViewModel - Logic and data
public partial class ProductViewModel(IProductService productService) : ObservableObject
{
    [ObservableProperty]
    private string _productName = "";

    [ObservableProperty]
    private decimal _price;

    [RelayCommand]
    private async Task LoadProduct(int id)
    {
        var product = await productService.GetProductAsync(id);
        ProductName = product.Name;
        Price = product.Price;
    }
}

// Page - Only UI
public class ProductPage(ProductViewModel vm) : UiPageElement(vm)
{
    protected override UiElement Build()
    {
        return new VStack(
            new Label().BindText(nameof(vm.ProductName), () => vm.ProductName),
            new Label().BindText(nameof(vm.Price), () => $"${vm.Price:F2}")
        );
    }
}

Don’t: Put Logic in Pages

// BAD - Logic in page
public class ProductPage(IProductService service) : UiPageElement(null)
{
    private Product? _product;

    protected override UiElement Build()
    {
        // Don't load data here!
        _product = service.GetProduct(1).Result; // Blocking call!

        return new Label().SetText(_product?.Name);
    }
}

Data Binding

Do: Use Proper Bindings

// One-way binding (ViewModel -> View)
new Label()
    .BindText(nameof(vm.Title), () => vm.Title)

// Two-way binding (ViewModel <-> View)
new Entry()
    .BindText(nameof(vm.SearchQuery), () => vm.SearchQuery, value => vm.SearchQuery = value)

// Command binding
new Button()
    .SetCommand(vm.SaveCommand)

Don’t: Forget Property Names

// BAD - Binding won't update when property changes
new Label()
    .SetText(vm.Title)  // Static text, never updates!

// GOOD
new Label()
    .BindText(nameof(vm.Title), () => vm.Title)

Layout Performance

Do: Use Appropriate Containers

// For simple vertical lists
new VStack(item1, item2, item3)

// For simple horizontal lists
new HStack(item1, item2, item3)

// For grid layouts
new Grid()
    .AddColumn(Column.Star)
    .AddColumn(Column.Auto)
    .AddRow(Row.Auto)

// For large scrollable lists
new ItemsList<T>()
    .SetItemsSource(items)
    .SetItemTemplate(item => new Label().SetText(item.Name))

Don’t: Over-Nest Layouts

// BAD - Too many nested containers
new VStack(
    new HStack(
        new VStack(
            new HStack(
                new Label().SetText("Deep!")
            )
        )
    )
)

// GOOD - Use Grid for complex layouts
new Grid()
    .AddColumn(Column.Star)
    .AddColumn(Column.Auto)
    .AddRow(Row.Auto)
    .AddChild(row: 0, column: 0, child: new Label().SetText("Simple!"))

Control Creation

Do: Create Controls in Build()

public class MyPage(MyViewModel vm) : UiPageElement(vm)
{
    protected override UiElement Build()
    {
        // Controls are created fresh each time Build() is called
        return new VStack(
            new Label().BindText(nameof(vm.Title), () => vm.Title),
            new Button().SetText("Click").SetCommand(vm.ClickCommand)
        );
    }
}

Don’t: Store Control References

// BAD - Don't cache controls
public class MyPage(MyViewModel vm) : UiPageElement(vm)
{
    private Label? _titleLabel; // Don't do this!

    protected override UiElement Build()
    {
        _titleLabel = new Label().SetText("Title");
        return _titleLabel;
    }

    public void UpdateTitle(string title)
    {
        _titleLabel?.SetText(title); // Don't do this!
    }
}

Custom Controls

Do: Use Partial Classes with GenerateShadowMethods

using PlusUi.core.Attributes;

[GenerateShadowMethods]
public partial class MyCustomButton : UiElement
{
    protected internal override bool IsFocusable => true;
    public override AccessibilityRole AccessibilityRole => AccessibilityRole.Button;

    // Properties with internal setter, public Set method
    internal string Text { get; set; } = "";
    public MyCustomButton SetText(string text)
    {
        Text = text;
        InvalidateMeasure();
        return this;
    }

    // Don't forget Bind method for every Set method!
    public MyCustomButton BindText(string propertyName, Func<string> getter)
    {
        RegisterBinding(propertyName, () => Text = getter());
        return this;
    }

    public override void Render(SKCanvas canvas)
    {
        base.Render(canvas);
        // Custom rendering
    }

    public override Size MeasureInternal(Size availableSize, bool dontStretch = false)
    {
        // Return desired size
        return new Size(100, 40);
    }
}

Don’t: Duplicate Base Properties

// BAD - Background already exists in UiElement!
public partial class MyControl : UiElement
{
    internal Color BackgroundColor { get; set; } // Don't redefine!

    public MyControl()
    {
        // GOOD - Use inherited SetBackground
        SetBackground(new SolidColorBackground(Colors.Blue));
    }
}

Async Operations

Do: Use Async Commands

public partial class MyViewModel : ObservableObject
{
    [ObservableProperty]
    private bool _isLoading;

    [RelayCommand]
    private async Task LoadDataAsync()
    {
        IsLoading = true;
        try
        {
            var data = await _service.GetDataAsync();
            Items = data;
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Don’t: Block the UI Thread

// BAD - Blocks UI
[RelayCommand]
private void LoadData()
{
    var data = _service.GetDataAsync().Result; // Blocking!
    Items = data;
}

Memory Management

Do: Dispose Resources

public class MyPage : UiPageElement, IDisposable
{
    private readonly Timer _timer;

    public MyPage(MyViewModel vm) : base(vm)
    {
        _timer = new Timer(OnTick, null, 0, 1000);
    }

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

Do: Use Page Lifecycle

public class MyPage(MyViewModel vm) : UiPageElement(vm)
{
    public override void Appearing()
    {
        base.Appearing();
        // Start animations, load data
    }

    public override void Disappearing()
    {
        base.Disappearing();
        // Stop animations, save state
    }
}

Margins and Padding

Do: Be Consistent

// Define constants
public static class Spacing
{
    public static readonly Margin Small = new(4);
    public static readonly Margin Medium = new(8);
    public static readonly Margin Large = new(16);
    public static readonly Margin Section = new(24);
}

// Use them consistently
new VStack(
    new Label().SetMargin(Spacing.Medium),
    new Button().SetMargin(Spacing.Medium)
)

Margin vs Padding

// Margin - Space OUTSIDE the element
new Button()
    .SetMargin(new Margin(10))  // Space around the button

// Padding - Space INSIDE the element
new Button()
    .SetPadding(new Margin(10)) // Space inside the button (around text)

Summary Checklist

  • Use MVVM: Logic in ViewModel, UI in Page
  • Always bind with property names for updates
  • Keep layout hierarchy shallow
  • Create controls in Build(), don’t cache references
  • Use async/await, never .Result or .Wait()
  • Clean up resources in Dispose or Disappearing
  • Define spacing constants for consistency
  • Partial class + [GenerateShadowMethods] for custom controls
  • Set + Bind method pairs for all properties

Back to top

PlusUi is licensed under the MIT License.