'XAML/MVVM help for implementing screen grid

As a complete beginner to XAML/MVVM I would like to request some help with implementing a 24x14 letter screen.
The XAML implementation is quite easy I think with something like

<Grid x:Name="ScreenGrid"
      RowDefinitions="*, *, *, *, *, *, *, *, *, *, *, *, *, *"                  
      ColumnDefinitions="*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*"/>

However what I am struggling with is the Data Binding.
I would like to write something like an "UpdateScreen()" Method, that populates that grid with data where

  • Every Grid-Cell has 1 Letter
  • Every letter has a color (red, green, ...)
  • Every letter is either big or small

My current ViewModel looks like this (with the help of Microsoft.Toolkit.Mvvm):
public partial class ScreenViewModel : ObservableObject
{
    [ObservableProperty]
    private ScreenText[] _screen; 
    //ScreenText consists of char value, Color color, bool isBig

    [ICommand]
    private void ButtonPressed(AppButton button)
    {
        System.Diagnostics.Debug.WriteLine($"Button {button} was pressed");
    }

    private void UpdateScreen()
    {
        [.?.]
    }
}

Which is supposed to communicate with the actual logic of the app, which generates a ScreenText[], that is passed back to the ViewModel.
How do I wire this up to the ScreenGrid?
Thanks for your help.



Solution 1:[1]

The solution for me (minimal example) looks like this:

In the XAML the grid gets defined like this

<Grid x:Name="ScreenGrid"
              RowDefinitions="*, *, *, *, *, *, *, *, *, *, *, *, *, *"
              ColumnDefinitions="*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*"/>

The ViewModel looks like this (with the help of Toolkit.Mvvm):

public partial class ScreenViewModel : ObservableObject
{
    const int LINES = 14;

    public ObservableCollection<ScreenText> Screen { get; } = new ObservableCollection<ScreenText>();

    public ScreenViewModel()
    {
        for (int i = 0; i < LINES; i++)
        {
            Screen.Add(new ScreenText());
        }
    }

    [ICommand]
    private void ButtonPressed(AppButton button)
    {
        System.Diagnostics.Debug.WriteLine($"Button {button} was pressed");

        //Do some business logic and manipulate the screen

        OnPropertyChanged(nameof(Screen));
    }
}

And finally the MainPage.Xaml.cs:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        //Setup Screen
        AddScreenCells();
    }

    private void AddScreenCells()
    {
        for (int row = 0; row < ScreenGrid.RowDefinitions.Count; row++)
        {
            for (int col = 0; col < ScreenGrid.ColumnDefinitions.Count; col++)
            {
                Label label = new Label();
                label.SetBinding(Label.TextProperty, $"{nameof(ScreenViewModel.Screen)}[{row}].{nameof(ScreenText.Letters)}[{col}]");

                ScreenGrid.Add(label, col, row);
            }
        }
    }
}

Solution 2:[2]

Here is an approach that programmatically adds cells as children of grid, with each cell bound to an element of a list in viewmodel.

Given:

public class ScreenText
{
    // I think binding to xaml requires string, not char.
    public string Value {
        get => value.ToString();
    }
    ...
}

and ScreenViewModel.cs:

public class ScreenViewModel : ObservableObject
{
    // List or ObservableCollection (not Array) usually used for Xamarin Forms.
    public ObservableCollection<ScreenText> Texts { get; } = new ObservableCollection<ScreenText>();
    ...
}

MainPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ManipulateGridChildren.MainPage">
    <Grid x:Name="ScreenGrid"
        RowDefinitions="*, *, *, *, *, *, *, *, *, *, *, *, *, *"                  
        ColumnDefinitions="*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*"/>
</ContentPage>

MainPage.xaml.cs:

using System.Collections.Generic;
using Xamarin.Forms;

namespace ManipulateGridChildren
{
    public partial class MainPage : ContentPage
    {
        const int NRows = 14;
        static int NColumns = 24;

        public MainPage(ScreenViewModel vm)
        {
            InitializeComponent();
            // So you can access from any method.
            VM = vm;
            AddCells(vm);
            BindingContext = vm;
        }

        private ScreenViewModel VM;
        // Used a dictionary, so can add cells in any order.
        private Dictionary<int, Label> cells = new Dictionary<int, Label>();

        private void AddCells(ScreenViewModel vm)
        {
            // Grid rows and columns are numbered from "0".
            for (int row = 0; row < NRows; row++) {
                for (int column = 0; column < NColumns; column++) {
                    var cell = AddCell(row, column, vm);
                }
            }
        }

        private Label AddCell(int row, int column, ScreenViewModel vm)
        {
            var index = CellKey(row, column);
            
            var cell = new Label();
            // NOTE: I think "Value" has to be a "string" not a "char".
            cell.SetBinding(TextProperty, $"Texts[{index}].Value");
            cell.TextColor = Color.Black;
            cell.BackgroundColor = Color.White;
            cell.HorizontalTextAlignment = TextAlignment.Center;
            cell.VerticalTextAlignment = TextAlignment.Center;
            
            ScreenGrid.Children.Add(cell, column, row);
            // This dictionary makes it easier to access individual cells later.
            cells[index] = cell;
            return cell;
        }

        // Assign unique key to each cell.
        private int CellKey(int row, int column)
        {
            return row * NColumns + column;
        }

        // Can use this to access an individual cell later.
        // For example, you might change some property on that cell.
        private Label GetCell(int row, int column)
        {
            return cell[CellKey(row, column)];
    }
}

ALTERNATIVE:

For anyone not using a viewmodel, see my answer here.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dokug
Solution 2 ToolmakerSteve