I want to implement: Button are located in a square (as a 2-dimensional NxN array). When you click on a button, all the buttons in one line and in one column are rotated. The number N is customizable. I started doing everything first in MainWindow.xaml.cs , advised to do everything normally and use MVVM . MainWindow.xaml.cs code:

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private Button[,] CreateButtons(int quantity) { Form.Rows = quantity; Form.Columns = quantity; Button[,] buttons = new Button[quantity, quantity]; for (int i = 0; i < quantity; i++) { for (int j = 0; j < quantity; j++) { buttons[i, j] = new Button(); buttons[i, j].Width = 100; buttons[i, j].Height = 20; buttons[i, j].Margin = new Thickness(5,80,0,0); buttons[i, j].Click += new RoutedEventHandler(new_button_click); } } return buttons; } void new_button_click(object sender, RoutedEventArgs e) { Button btn = sender as Button; if (btn != null) { var rotateTransform = btn.RenderTransform as RotateTransform; var transform = new RotateTransform(90 + (rotateTransform == null ? 0 : rotateTransform.Angle)); transform.CenterX = 50; transform.CenterY = 10; btn.RenderTransform = transform; } } private void AddToWrapPanel(int quantity, Button[,] buttons) { for (int i = 0; i < quantity; i++) for (int j = 0; j < quantity; j++) { Form.Children.Add(buttons[i, j]); } } private int GetQuantityButtons() { ComboBoxItem item = (ComboBoxItem)comboBox1.SelectedItem; int count = int.Parse((string)item.Content); return count; } private void СreateButton_Click(object sender, RoutedEventArgs e) { if (Form.Children.Count > 0) Form.Children.Clear(); int count = GetQuantityButtons(); Button[,] buttons = CreateButtons(count); AddToWrapPanel(count, buttons); } } 

Now I start to transfer everything.

XAML :

 <Window x:Class="Di.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:user="clr-namespace:Di" Title="Сейф" Height="715.6" Width="840" Left="250" Top="10" Background="Silver" ResizeMode="CanMinimize" TextOptions.TextFormattingMode="Display" Icon="Resources/Icon1.ico"> <Window.DataContext> <user:MainWindowModel /> </Window.DataContext> <Grid> <ItemsControl Margin="0,30,0,0"> <UniformGrid x:Name="Form"/> <WrapPanel Name="wrapPanel" Background="#FFF2F2F2" /> </ItemsControl> <Button Content="Старт" Height="23" HorizontalAlignment="Left" Margin="457,12,0,0" Name="createButton" VerticalAlignment="Top" Width="75" Click="СreateButton_Click" Command="{Binding Seter}" /> <ComboBox Height="23" HorizontalAlignment="Left" Margin="368,12,0,0" Name="comboBox1" VerticalAlignment="Top" Width="74" SelectedIndex="0" RenderTransformOrigin="0.5,0.739"> <ComboBoxItem Content="{Binding Three}" /> <ComboBoxItem Content="{Binding Four}" /> <ComboBoxItem Content="{Binding Five}" /> <ComboBoxItem Content="{Binding Six}" /> </ComboBox> <Label Content="{Binding Lvl}" HorizontalAlignment="Left" Margin="254,10,0,0" VerticalAlignment="Top"/> </Grid> 

VM code:

 public class MainWindowModel { public int Three { get; set; } public int Four { get; set; } public int Five { get; set; } public int Six { get; set; } public string Lvl { get; set; } public MainWindowModel() { Three = 3; Four = 4; Five = 5; Six = 6; Lvl = "Сложность (3 - 6):"; } private ICommand _seter; public ICommand Seter { get { return _seter ?? (_seter = new RelayCommand(() => { // действие при вызове команды })); } } } 

So far the only way .. Help, please move and finish my points. For example: how to get the RotationAngle property in the VM servicing the button? How to click on the "create" to generate an array of buttons and then work with them? How to access UniformGrid and associate the number of rows and columns with the selected int in the combobox ?

  • 2
    Now I will write if someone else does not have time to me. - VladD
  • @VladD Is it possible to pass int to UnirormGrid in this UnirormGrid ? <UniformGrid x:Name="Form" Rows="{Binding ElementName=comboBox1, Path=Value}" Columns="{Binding ElementName=comboBox1, Path=Value}"/> - Saint
  • The idea is possible, but it is better to bind to the VM. (Sorry, have not yet written the answer, but I will write.) - VladD

1 answer 1

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:

animated cartoon

  • Only my compiler swears a lot at that :) 'Why.BoardVM.cells' is a' field 'but it doesn’t contain a definition for Why.GameInfo' Why.VM.PropertyChanged 'is a' field 'but it is used like a' type ' - Saint
  • @Saint: Okay, let's get the string to swear. - VladD
  • @Saint: What is your version of the studio? I have everything compiled (VS2015). - VladD
  • I have Visual Studio 2012 - Saint
  • @Saint: Yyyyy! Well, yes, the old Studio, the old version of the language. Then we rewrite it like this: pastebin.com/GFYrQsX5 - VladD