Let's go first. Let's build a VM. We need a base class for a VM in which the implementation of INotifyPropertyChanged will be:
class VM : INotifyPropertyChanged { protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; RaisePropertyChanged(propertyName); return true; } protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; }
(If you use any MVVM framework, then you can already have the same base class defined.)
Now VM for one cell. What do we need to know? Angle of rotation - let's make a property from it with INPC. Row and column - these properties are immutable. And the team that will be called when the cell is activated. (It is also unchangeable.)
The action that will be performed when you click on a cell, the cell itself can not perform, since the rotation occurs in many cells. Therefore, the reaction to the action will be passed “from above” as a parameter. We get this code:
class CellVM : VM { public CellVM(int row, int column, Action<int, int> onActivate) { Row = row; Column = column; Activate = new RelayCommand(() => onActivate(row, column)); } double rotationAngle = 0; public double RotationAngle { get { return rotationAngle; } set { Set(ref rotationAngle, value); } } public int Row { get; } public int Column { get; } public ICommand Activate { get; } }
The next VM is the whole board. She will have to make a decision on the rotation of the cells. What data do we need here? Width and height are needed, and when changing you need to recreate the array of cells. We need the cells themselves, and since the cells will be replaced only as a whole, we take not the ObservableCollection<CellVM> , but simply the IEnumerable<CellVM> . You cannot expose a square array outside, nobody knows how to attach to it. Therefore, we put all the cells "merged" into one common sequence.
class BoardVM : VM { int width; public int Width { get { return width; } set { if (Set(ref width, value)) { GenerateCells(); } } } int height; public int Height { get { return height; } set { if (Set(ref height, value)) { GenerateCells(); } } } CellVM[,] cells; public IEnumerable<CellVM> AllCells => cells.Cast<CellVM>();
Further, when changing the number of rows or columns, we need to regenerate the cells.
void GenerateCells() { var cells = new CellVM[width, height]; for (int row = 0; row < height; row++) for (int column = 0; column < width; column++) cells[column, row] = new CellVM(row, column, OnCellActivate); ShuffleAngles(cells); // отбрасываем существующие клетки this.cells = cells; RaisePropertyChanged(nameof(AllCells)); }
... and set them a random starting angle:
static Random random = new Random(); void ShuffleAngles(CellVM[,] cells) { for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) cells[x, y].RotationAngle = random.Next(4) * 90; }
Now the function that is called when the cell is activated. We need to rotate all the cells in the same column and in the same row. In the second cycle, we skip the cell once rotated.
void OnCellActivate(int row0, int column0) { for (int row = 0; row < height; row++) Rotate(cells[column0, row]); for (int column = 0; column < width; column++) if (column != column0) Rotate(cells[column, row0]); } void Rotate(CellVM cellVM) { cellVM.RotationAngle = (cellVM.RotationAngle + 90) % 360; } }
Okay, the VM is more or less clear. Go to View.
We need ItemsControl , ItemsControl we want to show a sequence of items. Our sequence of elements is contained in the AllCells property.
<ItemsControl ItemsSource="{Binding AllCells}">
Next, we need the cells to fit into the UniformGrid . Let's UniformGrid as a carrier, at the same time we will tie the number of rows and columns:
<ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid IsItemsHost="True" Rows="{Binding Width}" Columns="{Binding Height}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel>
Next, how to show a separate cell? You want a Button , let it go. We write DataTemplate .
<ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:CellVM}"> <Button Command="{Binding Activate}"/>
Having launched the program, we see that the button is too adjacent to the cell borders, therefore we give it Margin="10" . Now, we need to somehow designate where the top and where the bottom. To do this, we draw an arrow up (but you have to do something more beautiful). The arrow will be rotated at an angle from the binding:
<ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:CellVM}"> <Button Command="{Binding Activate}" Margin="10" Padding="10"> <Path Data="M 0,1 L 1,0 L 2,1 M 1,2 L 1,0" Stroke="Black" Stretch="Uniform" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <RotateTransform Angle="{Binding RotationAngle}"/> </Path.RenderTransform> </Path> </Button> </DataTemplate> </ItemsControl.ItemTemplate>
It seems to be all.
</ItemsControl>
Now you need to specify the size of the field.
To do this, in a good way, you need to have another window (and only show it at the very beginning of the game), because changing the size of the field during the game is somehow wrong. But in our fast prototype, we close our eyes to it. (And then you have to redo it.)
So, we need information about how many rows and columns we can have. We return to the VM and start the class:
static class GameInfo { static public IEnumerable<int> PossibleColumnNumber { get; } = new[] { 3, 4, 5, 6 }; static public IEnumerable<int> PossibleRowNumber { get; } = new[] { 3, 4, 5, 6 }; }
For beauty, we need to initialize the values in BoardVM valid number. Find and change strings:
int width = GameInfo.PossibleColumnNumber.Min();
and
int height = GameInfo.PossibleRowNumber.Min();
Now View. We add two combo boxes and tags to them:
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Center"> <Label Target="{Binding ElementName=ColumnChooser}">Columns: </Label> <ComboBox Name="ColumnChooser" SelectedItem="{Binding Width}" ItemsSource="{x:Static vm:GameInfo.PossibleColumnNumber}"/> <Label Target="{Binding ElementName=RowChooser}">Rows:</Label> <ComboBox Name="RowChooser" SelectedItem="{Binding Height}" ItemsSource="{x:Static vm:GameInfo.PossibleRowNumber}"/> </StackPanel>
Entire MainWindow.xaml :
<Window x:Class="View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ViewModels" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" Title="Test" Height="350" Width="350"> <Grid d:DataContext="{d:DesignInstance Type=vm:BoardVM, IsDesignTimeCreatable=False}"> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ItemsControl ItemsSource="{Binding AllCells}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid IsItemsHost="True" Rows="{Binding Width}" Columns="{Binding Height}"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:CellVM}"> <Button Command="{Binding Activate}" Margin="10" Padding="10"> <Path Data="M 0,1 L 1,0 L 2,1 M 1,2 L 1,0" Stroke="Black" Stretch="Uniform" RenderTransformOrigin="0.5,0.5"> <Path.RenderTransform> <RotateTransform Angle="{Binding RotationAngle}"/> </Path.RenderTransform> </Path> </Button> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Center"> <Label Target="{Binding ElementName=ColumnChooser}">Columns: </Label> <ComboBox Name="ColumnChooser" SelectedItem="{Binding Width}" ItemsSource="{x:Static vm:GameInfo.PossibleColumnNumber}"/> <Label Target="{Binding ElementName=RowChooser}">Rows:</Label> <ComboBox Name="RowChooser" SelectedItem="{Binding Height}" ItemsSource="{x:Static vm:GameInfo.PossibleRowNumber}"/> </StackPanel> </Grid> </Window>
Now we need to attach the VM to the View. The best way to do this is not in XAML, but in App.xaml.cs (see here ). We write:
public partial class App : Application { BoardVM boardVM = new BoardVM(); protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new MainWindow() { DataContext = boardVM }.Show(); } }
and remove from the App.xaml StartupUri .
Compile, run. Immediately we see an empty field. A flaw, we did not generate a field in the BoardVM constructor! We fix:
public BoardVM() { GenerateCells(); }
Here's what I got:

inttoUnirormGridin thisUnirormGrid?<UniformGrid x:Name="Form" Rows="{Binding ElementName=comboBox1, Path=Value}" Columns="{Binding ElementName=comboBox1, Path=Value}"/>- Saint