2
votes

I built a very basic Sudoku app in C#, XAML, and MVVM-Light. The MainWindow has a 9x9 grid and 81 Textbox for each cell, and the MainViewModel has 81 int properties. Not pretty. The Sudoku table does not look like a classic table, there's no "thicker border" to differentiate each quadrant.

Now, I want to move on to the next level and make it pretty and practical.

Every cell content must be ?responsive? ie resize (font size) based on the width/height of the window. I already started work on a CellUserControl that handles font color, background color, etc.

What should be the base for the Main window ? Grid, Table, ListBox, Custom ?

  • I tried Grid and Table but usually ending up bumping to a dead end. The XAML becomes very verbose and repetitive.
  • I remember MS showing of the power of WPF with ListBox displayed as a carousel, just by applying styles. Is this a valid path ?
  • Custom Control: is it overkill or it exists exactly for this situation ?

By the way, I'll be leveraging MVVM-Light and Data Binding a lot.

3
Use grid. XAML is verbose and repetitive by its very nature.Leo Bartkus
Also, don't be afraid to construct the grid and cells using loops in code-behind if you want to avoid xaml's verbosity.Leo Bartkus
How do I take care of the lines ?Patrice Calvé
either put small rows and columns between each cell column, or make the lines by putting an outline in the cell using a rectangle with no margin on it. If you want to do the thicker border between each box of cells, you can use a solid rectangle to fill a thin row/column and use rowspan/columnspan to make it extend to the length of the whole boardLeo Bartkus

3 Answers

2
votes

Since all cells should have the same size, you could also use a UniformGrid. As Leo Bartkus suggested, you could use code-behind to generate the cells and text boxes. To do this, start by creating a placeholder for the Sudoku table in XAML:

<!-- Placeholder for Sudoku table (filled in code-behind) -->
<Border x:Name="SudokuTable" />

Assuming you're using a Window, here's the code-behind:

public partial class MainWindow : Window
{
    private const int InnerWidth = 3;
    private const int OuterWidth = InnerWidth * InnerWidth;

    private const int Thin = 1;
    private const int Thick = 3;

    public MainWindow()
    {
        InitializeComponent();
        InitializeViewModel();
        InitializeSudokuTable();
    }

    public SudokuViewModel ViewModel => (SudokuViewModel)DataContext;

    private void InitializeViewModel()
    {
        DataContext = new SudokuViewModel(OuterWidth);
    }

    private void InitializeSudokuTable()
    {
        var grid = new UniformGrid
        {
            Rows = OuterWidth,
            Columns = OuterWidth
        };

        for (var i = 0; i < OuterWidth; i++)
        {
            for (var j = 0; j < OuterWidth; j++)
            {
                var border = CreateBorder(i, j);
                border.Child = CreateTextBox(i, j);
                grid.Children.Add(border);
            }
        }

        SudokuTable.Child = grid;
    }

    private static Border CreateBorder(int i, int j)
    {
        var left = j % InnerWidth == 0 ? Thick : Thin;
        var top = i % InnerWidth == 0 ? Thick : Thin;
        var right = j == OuterWidth - 1 ? Thick : 0;
        var bottom = i == OuterWidth - 1 ? Thick : 0;

        return new Border
        {
            BorderThickness = new Thickness(left, top, right, bottom),
            BorderBrush = Brushes.Black
        };
    }

    private TextBox CreateTextBox(int i, int j)
    {
        var textBox = new TextBox
        {
            VerticalAlignment = VerticalAlignment.Center,
            HorizontalAlignment = HorizontalAlignment.Center
        };

        var binding = new Binding
        {
            Source = ViewModel,
            Path = new PropertyPath($"[{i},{j}]"),
            Mode = BindingMode.TwoWay
        };

        textBox.SetBinding(TextBox.TextProperty, binding);

        return textBox;
    }
}

The nested loop creates each Border and TextBox for the 81 cells. The border's thicknesses are determined based on the current cell's position. This will give you the typical Sudoku table look.

The text boxes are data-bound to the two-dimensional indexer property of the view model. Here's the view model:

public class SudokuViewModel : ViewModelBase
{
    private readonly string[,] _values;

    public SudokuViewModel(int width)
    {
        _values = new string[width, width];
    }

    public string this[int i, int j]
    {
        get => _values[i, j];
        set => Set(ref _values[i, j], value);
    }
}

This indexer returns a string, but you may want to change it to an integer and do the appropriate conversions and error checks. In any case, it's using MVVM Light to raise the PropertyChanged event when the indexer property is updated.

I've created a repository with my solution here: https://github.com/redcurry/Sudoku.

0
votes

Here's an example of how I would do sudoku with a grid...

<Grid>


 <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="5"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="5"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <TextBox x:Name="textBox00" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"/>
    <TextBox x:Name="textBox00_Copy" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1" Grid.Column="1"/>
    <TextBox x:Name="textBox00_Copy1" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="2"/>
    <TextBox x:Name="textBox00_Copy2" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="4"/>
    <TextBox x:Name="textBox00_Copy3" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="5"/>
    <TextBox x:Name="textBox00_Copy4" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1" Grid.Column="6"/>
    <TextBox x:Name="textBox00_Copy5" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="2" Grid.Row="1"/>
    <TextBox x:Name="textBox00_Copy6" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1" Grid.Column="4" Grid.Row="1"/>
    <TextBox x:Name="textBox00_Copy7" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1" Grid.Column="5" Grid.Row="1"/>
    <TextBox x:Name="textBox00_Copy8" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="1" Grid.Row="1"/>
    <TextBox x:Name="textBox00_Copy9" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="1" Grid.Row="2"/>
    <TextBox x:Name="textBox00_Copy10" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="2" Grid.Row="2"/>
    <TextBox x:Name="textBox00_Copy11" TextWrapping="Wrap" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Text="1"  Grid.Column="4" Grid.Row="2"/>
    <Rectangle Grid.Column="3" Fill="#FF663003" Grid.RowSpan="12"/>
    <Rectangle Grid.Column="7" Fill="#FF663003" Grid.RowSpan="12"/>
    <Rectangle Grid.Row="3" Fill="#FF663003" Grid.ColumnSpan="12"/>
    <Rectangle Grid.Row="7" Fill="#FF663003" Grid.ColumnSpan="12"/>

This is just the default textbox style. You could make it different with a usercontrol, or you could make a custom style for the textbox by loading the xaml in Blend for Visual studio, rightclick the textbox and choose Edit Template.. -> Edit a Copy

0
votes

Great answers everyone, your ideas made me focus on a goal :)

Here's where I'm at, as of now.

XAML: I'm using the verbose XAML approach and a Grid.

Grid: 11x11 - (9x9 for the Sudoku cells + 2x2 for the borders).

<Grid Grid.Row="1" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>

            <ColumnDefinition Width="2px"/>

            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>

            <ColumnDefinition Width="2px"/>

            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>

            <RowDefinition Height="2px"/>

            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>

            <RowDefinition Height="2px"/>

            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Rectangle Grid.Row="0" Grid.Column="3" Grid.RowSpan="11" Fill="Black"></Rectangle>
        <Rectangle Grid.Row="0" Grid.Column="7" Grid.RowSpan="11" Fill="Black"></Rectangle>
        <Rectangle Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="11" Fill="Black"></Rectangle>
        <Rectangle Grid.Row="7" Grid.Column="0" Grid.ColumnSpan="11" Fill="Black"></Rectangle>

        <local:CellUserControl Grid.Row="0" Grid.Column="0"  DataContext="{Binding Path=Cells[0], Source={StaticResource Locator}}"/>
        <local:CellUserControl Grid.Row="0" Grid.Column="1"  DataContext="{Binding Path=Cells[1], Source={StaticResource Locator}}"/>
...

I'm lazy, so I ended up using an Excel Spreadsheet to enumerate the 81 cells, and use a combination Floor.Math, MOD and Concatenate :)

The next challenge was the refactoring of the 81 MVVM Properties to something more trivial. In XAML: The syntax is {Binding Path=Cells[0]} and choose to put (for now) the property in the ViewModelLocator.

    public IList<CellViewModel> Cells
    {
        get
        {
            return new List<CellViewModel>(ServiceLocator.Current.GetAllInstances<CellViewModel>());
        }
    }

The XAML and code are clean. I like it so far. I'm still juggling the proper location for the IList - should it remain in the ViewModelLocator or should it be actually in the MainViewModel ? I guess, to answer this question, I'll have to do some unit testing.

Many thanks.