MVVM and binding

The controls expose the grid's state as bindable properties and take ICommands for what the user does, so a view model can drive a grid without a reference to the control. Columns are declared in code, on a view the view model owns.

Where the columns live

There is no column markup. A view model creates the GridView — columns with typed lambdas, a source over its collection — and exposes it; the view binds MainView to it. That keeps the declaration in one place, compiled and refactorable, and the view model can switch to a card board or a pivot by exposing another view.

OrdersViewModel.cs
public sealed class OrdersViewModel
{
    public ObservableCollection<Order> Orders { get; } = new();

    // The columns live in the view model, as code: one declaration, typed lambdas.
    public GridView<Order> OrdersView { get; }

    public Order? SelectedOrder { get; set; }
    public bool IsLocked { get; set; }
    public GridFilterState? Filters { get; set; }
    public ICommand OpenOrderCommand { get; }
    public IGridEditPolicy EditPolicy { get; }

    // Handed in by the grid (OneWayToSource): scroll, select and edit without the control.
    public IGridActions? GridActions { get; set; }

    public OrdersViewModel()
    {
        OrdersView = new GridView<Order>(
            [
                TypedColumn.Number<Order>("No.", o => o.Id, format: "0").WithKey("id"),
                Column.Text<Order>("Customer", o => o.Customer)
                      .WithKey("customer").Editable((o, v) => o.Customer = v).Fill(),
                TypedColumn.Number<Order>("Amount", o => (double)o.Amount,
                                          (o, v) => o.Amount = (decimal)(v ?? 0), "N2").WithKey("amount"),
            ],
            new ObservableItemsSource<Order>(Orders))
            .AsDataGrid();

        OpenOrderCommand = new RelayCommand(item => Open((Order)item!));
        EditPolicy = new OrderEditPolicy(OrdersView);
    }

    public void ShowNewest(Order order)
    {
        GridActions?.ScrollTo(order);
        GridActions?.SelectItems(new[] { order });
    }

    private static void Open(Order order) { }
}

WPF

OrdersView.xaml
<ToolBar DockPanel.Dock="Top">
  <Button Content="Undo" Command="{Binding Api.Commands.UndoCommand, ElementName=Orders}" />
  <Button Content="Copy" Command="{Binding Api.Commands.CopyCommand, ElementName=Orders}" />
</ToolBar>

<arion:ArionGrid x:Name="Orders"
                 MainView="{Binding OrdersView}"
                 SelectedItem="{Binding SelectedOrder}"
                 IsReadOnly="{Binding IsLocked}"
                 FilterState="{Binding Filters}"
                 RowActivatedCommand="{Binding OpenOrderCommand}"
                 EditPolicy="{Binding EditPolicy}" />

The namespace is clr-namespace:ArionUI.Rendering.Wpf;assembly=ArionUI.Rendering.Wpf. The toolbar binds to the grid's own commands through Api.Commands; their CanExecute follows the grid's state.

Actions hands the view model an IGridActions — scroll to an item, select items, begin an edit, set a value, clear filters — through a OneWayToSource binding. WPF markup refuses that binding because the property has no CLR setter, so it is made in code:

OrdersView.xaml.cs
public OrdersView()
{
    InitializeComponent();

    // WPF markup refuses Actions (it has no setter), so this one binding is made in code.
    Orders.SetBinding(ArionGrid.ActionsProperty,
        new Binding(nameof(OrdersViewModel.GridActions)) { Mode = BindingMode.OneWayToSource });
}

Avalonia

OrdersView.axaml
<arion:ArionGrid MainView="{Binding OrdersView}"
                 SelectedItem="{Binding SelectedOrder}"
                 IsReadOnly="{Binding IsLocked}"
                 RowActivatedCommand="{Binding OpenOrderCommand}"
                 EditPolicy="{Binding EditPolicy}"
                 Actions="{Binding GridActions, Mode=OneWayToSource}" />

Avalonia markup accepts the Actions binding directly. With compiled bindings, give the view an x:DataType as usual.

Bindable properties

PropertyMeaning
MainViewThe view: table, cards, tree, pivot, chart, timeline.
ItemsSourceThe data. Replacing it keeps the columns; an ObservableCollection is observed. See Data sources.
SelectedItem, SelectedItemsThe selected rows' items. Two-way by default.
CurrentItem, CurrentCellThe item and the cell (GridCellRef) under the keyboard cursor. Two-way by default.
SelectionModeCell or Row.
FilterStateThe complete filter state (GridFilterState), mirrored when the user filters.
VisualTheme, ContextTheme; texts and cultures (see Localisation).
IsReadOnly, IsLoading, LoadingTextWrite protection; the drawn loading state.
EmptyStateTitle, EmptyStateMessage, EmptyStateActionLabel, EmptyStateCommandThe drawn empty state and its button.
FrozenColumns, FrozenRows, ShowFilterRow, ZoomLayout.
CanUserSortColumns, CanUserResizeColumns, CanUserReorderColumns, CanUserFilterColumns, CanUserGroupColumnsWhich gestures the user has.
ActionsRead by the view model, OneWayToSource.

Commands into the grid

PropertyExecuted with
RowActivatedCommandThe row's data item, on a double-click.
CellEditStartingCommandThe cell as a GridCellRef. It observes; it cannot veto — that is the edit policy's job.
CellEditCommittedCommandGridCellEditCommittedEventArgs: the hook for saving.
CellContextMenuRequestedCommandCellContextMenuEventArgs, whose entry list you may change.
ClearFiltersRequestedCommandNo parameter. Bound, it replaces the grid's own clearing.
EditPolicyNot a command: the IGridEditPolicy asked before every edit. See Editing and validation.

The same inputs exist without a binding on grid.Api.CommandInputs.

WinForms

ArionGridControl carries the same properties as plain .NET properties and implements INotifyPropertyChanged, so it can take part in WinForms data binding. The command inputs and the edit policy are properties there too. It is built from source for now; see Hosting and rendering.