WPF - Game grid - c#

i have wpf application, a game of sudoku. I have a model and user control for a game grid (9x9 squares). Because my knowledge of binding is limited and i had not enough time, i decided to make it old way without databinding and manualy synchronize model and view. However it is very unclean and synchronization problems appear.
I decided to convert to proper databinding.
I suppose my grid should be something like itemscontrol (listbox or combobox) but instead of linear list it would layout its items into two dimensional grid.
I am new to binding and i have no idea how to achieve my goal. I have model class which has some general info about current puzzle and contains collection of cells. Each cell has its own propertiues like value, possibilities, state etc.
I have user control of entire grid and user control for each individual cell. I need grid to bind to some properties of my model (eg, disabled,enabled,sudoku-type) and each cell to bind to my corresponding cell - value, background etc.
EDIT: Cell are in observable collection. Each cell has X and Y property. I think they should somehow bind to Grid.Row and Grid.Column properties.
Can you please point me some direction how to continue? Especially creating that itemscontrol and how to bind to it?
Thank you.

1) Should it be observable? - no, you could use for example List<List<CellModel>>()
2) If you want to still use array[,] - you may need some kind of converter if you want to use ItemsControl
3) You can use items control with ItemTemplate wich will be an Items control too
hope this will help
Edit 1: Lets create a converter from point 2...
public class ArrayConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var val = value as CellModel[,];
if (val == null) return null;
return ToEnumerable(val);
}
private IEnumerable<IEnumerable<CellModel>> ToEnumerable(CellModel[,] array)
{
var count = array.GetLength(0);
for (int i = 0; i < array.GetLength(0); ++i)
{
yield return GetLine(array, i);
}
}
private IEnumerable<CellModel> GetLine(CellModel[,] array, int line)
{
var count = array.GetLength(1);
for (int i = 0; i < count; ++i)
{
yield return array[line, i];
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
Edit 2: Your xaml could look like (see point 3):
<Grid>
<Grid.Resources>
<Converters:ArrayConverter x:Key="ArrayConverter"/>
</Grid.Resources>
<ItemsControl
ItemsSource="{Binding CellArray, Mode=OneWay, Converter={StaticResource ArrayConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl
ItemsSource="{Binding ., Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- TODO: Add cell template here -->
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Edit 3: added line <StackPanel Orientation="Horizontal" IsItemsHost="True"/> - this will make your lines to be rendered horisontal
Edit 4: your view model could now be something like this:
public class GameViewModel : ViewModelBase
{
public void Load()
{
var array = new CellModel[9, 9];
for (int i = 0; i < 9; ++i)
{
for (int j = 0; j < 9; ++j)
{
array[i, j] = new CellModel()
{
//TODO: init properties of the cell[i, j]
};
}
}
this.CellArray = array;
}
CellModel[,] _CellArray;
public CellModel[,] CellArray
{
get
{
return _CellArray;
}
private set
{
if (_CellArray == value) return;
_CellArray = value;
NotifyPropertyChanged("CellArray");
}
}
}
Let me know if something is still unclear
Edit 5: Depends on your last edit, lets change our XAML:
<ItemsControl
ItemsSource="{Binding CellArray, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid
Grid.Column="{Binding X}"
Grid.Row="{Binding Y}">
<!-- TODO: -->
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid IsItemsHost="True">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

You don't need to use an ObservableCollection to bind to a collection, however it is recommended if you want to CollectionChange notification to be automatically implemented.
This means if you update your collection (for example, clearing it and starting a new Game), it will automatically tell the UI that the collection has changed and the UI will redraw itself with the new elements.
You can also Linq statements with ObservableCollections to find a specific element. For example, the following will return the first cell that matches the specified criteria, or null if no item is found
var cell = MyCollection.FirstOrDefault(
cell => cell.Y == desiredRowIndex && cell.X == desiredColumnIndex);

Related

Gridview element’s background depending on whether the binded object is an element of the list in application for UWP

Welcome.
It’s the first time, that I can’t find solution on this website and I have to ask own question. I have list (List1) of objects my class Cell. This objects are binded to a Gridview. GridviewElements has Grid, which background I want to change each time the binded object is being added to or removed from other list (List2).
Firstly, I was trying to achieve it by using WinRTMultiBinding
(https://github.com/Verbon/WinRTMultiBinding), but it seems that can only bind simple types, not Cell and list of it. Then I started to trying initialize IValueConverter object in VievModel, but couldn’t bind it to {Binding Converter}. After that I decided to declare and pass List2 to the converter as a field, but it didn’t changed through the time. My last try, was declaring static field in ViewModel class that contains current reference, but as the GridViewElement binds only Cell object which is not changing, the PropertyChanged event is not raised, so the colour stays the same.
I’m now thinking about declaring a new property in Cell class that represents brush, or new list of cell brushes to be bind as background, but I hope there is better solution. And there is my question: do You know any?
My XAML code:
<GridView Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Cells}">
<GridView.ItemTemplate>
<DataTemplate>
<Border Name="Border" BorderThickness="2" Width="30" Height="30" CornerRadius="2" Tag="{Binding}"
BorderBrush="{Binding Converter={StaticResource CellToBrush}}"
PointerPressed="OnCrosswordGridViewItemPointerPressed">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock Name="Character" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{StaticResource ForegroundMain}" Text="{Binding Char}"/>
</Grid>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
There is my VM:
It contains
Crossword object - model for that page which contains list of Cells
List1 (named Cells)
VM's deffinition of it (named the same as in model)
Deffinition of StartCell that represents first Cell that user
presses down
Deffinition of ObservableCollection of Cell SelectedCells - this is the List2
Crossword crossword;
public List<Cell> Cells
{
get { return crossword.Cells; }
}
Cell startCell;
public Cell StartCell
{
set
{
if(value != startCell)
{
startCell = value;
OnPropertyChanged("StartCell");
}
}
get { return startCell; }
}
public ObservableCollection<Cell> SelectedCells
{
get;set;
}
And methods in code behind:
void OnCellPointerPressed(object sender, PointerRoutedEventArgs args)
{
if (args.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
CreateCrosswordPageVM vm = (CreateCrosswordPageVM)DataContext;
vm.StartCell = (Cell)((Border)sender).Tag;
}
}
void OnCrosswordGridViewItemPointerMoved(object sender, PointerRoutedEventArgs args)
{
CreateCrosswordPageVM vm = (CreateCrosswordPageVM)DataContext;
Cell currentCell = (Cell)((Border)sender).Tag;
if (currentCell.Row == vm.StartCell.Row && currentCell.Column > vm.StartCell.Column)
vm.SelectedCells = new System.Collections.ObjectModel.ObservableCollection<Cell>(vm.Cells.FindAll(x=>x.Row == currentCell.Row && x.Column<= currentCell.Column && x.Column>= vm.StartCell.Column));
else if (currentCell.Column == vm.StartCell.Column && currentCell.Row > vm.StartCell.Row)
vm.SelectedCells = new System.Collections.ObjectModel.ObservableCollection<Cell>(vm.Cells.FindAll(x=>x.Column== currentCell.Column && x.Row <= currentCell.Row && x.Row >= vm.StartCell.Row));
}
First method passes the "pressed" Cell to ViewModel's StartCell, second checks which Cells lies between StartCell and current Cell (over which the pointer is) and passes them to SelectedCells.
And about the colours. I wanted to hardcode it in Converter and later when it works, pass them as Converter parameter or daclare is as resources in ResourceDictionary

Center aligning GridView items in last row

I want to implement a GridView which takes 3 items in a row, and if the number of items are 2 in last row, then the last row items should be aligned center instead of being left-aligned. Here are a couple of images to explain what I want to achieve.
Currently my implementation looks like
.
And this is what I want to achieve.
Any help would be appreciated.
There are many ways realizing the feature that you mentioned.
To summarize it, you need to inherit GridView and override MeasureOverride ArrangeOverride method to re-calculate each Rect of Panel's children. This way is complex. For more info you could refer to
XAML custom panels overview.
And you could also use PrepareContainerForItemOverride method to re-layout the item directly.
<local:VariableGrid
x:Name="MyGridView"
SelectionMode="Single"
IsSwipeEnabled="False">
<local:VariableGrid.ItemTemplate >
<DataTemplate>
<StackPanel BorderBrush="Red" BorderThickness="3" Height="200" Width="200" Margin="20">
</StackPanel>
</DataTemplate>
</local:VariableGrid.ItemTemplate>
<local:VariableGrid.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid
Orientation="Horizontal"
VerticalAlignment="Top"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollMode="Disabled"
MaximumRowsOrColumns="4">
</VariableSizedWrapGrid>
</ItemsPanelTemplate>
</local:VariableGrid.ItemsPanel>
</local:VariableGrid>
VariableGrid.cs
public sealed class VariableGrid : GridView
{
public VariableGrid()
{
this.DefaultStyleKey = typeof(VariableGrid);
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
var list = this.ItemsSource as List<string>;
var griditem = element as GridViewItem;
for (var t = ((list.Count - list.Count % 4)); t < list.Count; t++)
{
if (item as string == list[t])
{
if (griditem != null)
{
VariableSizedWrapGrid.SetColumnSpan(griditem, 2);
}
}
}
base.PrepareContainerForItemOverride(element, item);
}
}
However, this simple way can not fit all the scenario.

Check a large group of checkboxes

I want to be able to say:
Get the first textblock, then the first checkbox, both with the number 1 in their name.
Then if the checkbox is checked, then the textblock can be populated.
See code:
for (int i = 1; i < 10; i++)
{
TextBlock a = (this.FindName(string.Format("tb_{0}", i)) as TextBlock);
CheckBox b = (this.FindName(string.Format("ck_{0}", i)) as CheckBox);
if (b.IsChecked.HasValue)
{
if (a != null) a.Text = data.ArrayOfSensors[i].ToString();
}
else
{
if (a != null) a.Text = data.ArrayOfSensors[0].ToString();
}
}
So when the checkbox is enabled, the textblock will be populated with the index from the array.
Many thanks!
EDIT: A slightly better explanation:
The textblocks are named: tb_1, tb_2 etc
The Checkboxes are named: cb_1, cb_2 etc
The array is:
[0] 0
[1] 100
[2] 150
The number is what they all have in common. So I can use a for loop with i as a common variable for each. I also have about 50 textboxes and Comboboxes and don't want to write each one out individually.
EDIT: My ComboBoxes and Textblocks are created on Xaml code like this:
<CheckBox x:Name="Cb_1" Width="15" Height="15" Margin="349,53,127,164" IsChecked="True" />
<TextBlock x:Name="tb_1" Text="80" Height="20" Width="20" Margin="266,35,205,177" />
Its hard to answer without seeing what your XAML looks like, however it sounds like you may be trying to use WPF like it is WinFirms.
To build an interface like this in WPF, you should start by creating a custom class to hold your data, and then use an ItemsControl to render your collection of data.
For example, your class might look something like this
public class SensorData() : INotifyPropertyChanged
{
// should implement INotifyPropertyChanged of course
public string Text { get; set; }
public bool IsChecked { get; set; }
}
And an ObservableCollection<SensorData> might be rendered using an <ItemsControl> with a ItemsPanelTemplate containing both a CheckBox and a TextBox
<ItemsControl ItemsSource="{Binding MyCollectionOfSensorData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Checked="{Binding IsChecked}" />
<TextBlock Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This will loop through the collection of SensorData objects, and render a CheckBox and TextBox for each one. If you want to do any manipulation of the data from the code-behind, you only need to modify the properties of the SensorData objects.
For example, you could have a loop that goes
for (int i = 0; i < MyCollectionOfSensorData.Length; i++)
{
SensorData item = MyCollectionOfSensorData[i];
if (item.IsChecked)
item.Text = data.ArrayOfSensors[i].ToString();
else
item.Text = "0";
}
And there would be no interaction with the UI objects at all.

How to manipulate individual group headers of gridview

The following scenario:
I use a gridview to present grouped data.
I added a TextBlock to the headertemplate which
should contain the number of items in this group. (For example)
( Edit: In my scenario i show always 6 items and want to show the overflow in the TextBlock children of my HeaderTemplate )
How can i access the individual group headers from code to manipulate this TextBlock?
Here is an example of the result:
And here a simplified example of my GroupHeaderTemplate:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock x:name="overflow"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
So i want to access and manipulate the "overflow" item individually for each generated group!
Here's what you really want.
First edit the GroupStyle HeaderTemplate
<GridView.GroupStyle>
<GroupStyle HidesIfEmpty="True">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Width="500" Margin="5,0,0,5">
<TextBlock HorizontalAlignment="Left">
<Run Text="{Binding Name}" />
<Run FontFamily="Segoe Ui Symbol" Text="" />
</TextBlock>
<TextBlock HorizontalAlignment="Right">
<Run Text="{Binding Children.Count, FallbackValue=0}" />
<Run Text="Items" />
</TextBlock>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid />
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</GridView.GroupStyle>
please note I am using a VaraibleSizedWrapGrid
Next, handle the GridView's Loaded Event
class SubtractConverter : IValueConverter
{
public double Amount { get; set; }
public object Convert(object v, Type t, object p, string l)
{ return System.Convert.ToDouble(v) - Amount; }
public object ConvertBack(object v, Type t, object p, string l)
{ throw new NotImplementedException(); }
}
private void GridView_OnLayoutUpdated(object sender, RoutedEventArgs e)
{
var grid = sender as GridView;
var converter = new SubtractConverter { Amount = 5 * 2 /* padding x2 */ };
foreach (GroupItem group in (grid.ItemsPanelRoot as Panel).Children)
{
var result = VisualTreeHelper.GetChild(group, 0);
while (!(result is Grid))
result = VisualTreeHelper.GetChild(result, 0);
var items = (result as Panel).Children.OfType<ItemsControl>()
.First().ItemsPanelRoot;
var binding = new Binding
{
Path = new PropertyPath("ActualWidth"),
Mode = BindingMode.OneWay,
Converter = converter,
Source = items,
};
var header = (result as Panel).Children.OfType<ContentControl>()
.First().ContentTemplateRoot as FrameworkElement;
header.SetBinding(FrameworkElement.WidthProperty, binding);
}
}
And, presto! Now your header is perfectly sized to the width of the items in the group.
Things to remember (as the designer):
Your grouped items might be as narrow as a single column. Solve this with TextTrimming in the Header TextBoxes.
Your grouped items might be wider than the monitor and # items might be off-screen. Solve this with a MinWidth on the containing grid.
Best of luck!
I finally managed to get my hands on the desired Element by using the VisualTreeHelperExtensions
First of all you have to install XamlToolkit via Nuget
Afterwards add a using directive for the Extensions:
using WinRTXamlToolkit.Controls.Extensions;
Now you can use several more methods on your ui elements , one of them is getDescendantsByType()
which i use to get all textblock elements hold by my gridview. I added a tag to my overflow textblocks which i check for when iterating through the gridviews descendants, see for yourself:
private void ItemsGridView_Loaded(object sender, RoutedEventArgs e)
{
foreach (TextBlock element in this.myGridView.GetDescendantsOfType<TextBlock>())
{
if(element.Tag != null && element.Tag.Equals("itemCountBlock")){
element.Text = "Finally solved!";
}
}
}
This should work out for any ui element and any property one would wanna change.

How do I bind a simple array?

I'm making a scrabble-like game in WPF C#. As of current I've got the AI Algorithm working with operates on a "Model Game Board", a variable string[15,15]. However over the past few days I'm stuck at producing a GUI to display this Array as a GameBoard.
As of current I've got the following XAML Code:
My "MainWindow" which contains:
A button:
<Button Click="Next_TurnClicked" Name="btnNext_Turn">Next Turn</Button>
A UserControl: Which is the Game Board(GameBoard is also another UserControl) and the Player's Rack
<clr:Main_Control></clr:Main_Control>
Then Inside my UserControl I have:
<DockPanel Style ="{StaticResource GradientPanel}">
<Border Style ="{StaticResource ControlBorder}" DockPanel.Dock="Bottom">
<ItemsControl VerticalAlignment="Center" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Height ="Auto" Name="ListBox" Orientation="Horizontal" HorizontalAlignment="Center">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<clr:Rack_Cell_Sender x:Name="Player_Tile_1" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_2" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_3" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_4" ></clr:Rack_Cell_Sender>
<clr:Rack_Cell_Sender x:Name="Player_Tile_5" ></clr:Rack_Cell_Sender>
</ItemsControl>
</Border>
<Viewbox Stretch="Uniform">
<Border Margin="5" Padding="10" Background="#77FFFFFF" BorderBrush="DimGray" BorderThickness="3">
<Border BorderThickness="0.5" BorderBrush="Black">
<clr:GameBoard>
</clr:GameBoard>
</Border>
</Border>
</Viewbox>
</DockPanel>
clr:GameBoard is a ItemsControl with its ItemsPanelTemplate as a UniformGrid
<Style TargetType="ItemsControl">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid IsItemsHost="True" Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Rows="15" Columns="15" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Words:Cell>
</Words:Cell>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<ItemsControl Name="BoardControl" ItemsSource="{DynamicResource CellCollectionData}">
</ItemsControl>
So my question is:
How do I get my array[,] to the ItemsControl? I have no ideas how to DataBind, I've read a couple of tutorials but I'm only starting to get what a DataContext is.
How will I refresh the Board after each turn? I don't want the board updating before btnNext_Turn is clicked.
How do I Update the ModelBoard after user inputs new word into UI and btn_Next_Turn is clicked?
I'm a beginner in coding and this is my first real project in C# and WPF. My teacher knows neither WPF or C# so you guys on the StackoverFlow community has been a great help over the past few weeks, especially helping me out on SQL.
Please, any help will be much appreciated!
UPDATE:
Thanks Erno for the quick response! Yea, I got an error for EventHandler so I swapped it out for PropertyChangedEventHandler and that stopped the errors.
public partial class GameBoard : UserControl
{
TileCollection RefreshTiles = new TileCollection();
public GameBoard()
{
InitializeComponent();
}
public void getBoard()
{
string[,] ArrayToAdd = InnerModel.ModelBoard;
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Tile AddTile = new Tile();
AddTile.Charater = ArrayToAdd[i, j];
AddTile.X = i;
AddTile.Y = j;
RefreshTiles.Add(AddTile);
}
}
}
}
So when I run debug I can see the RefreshTiles Collection being filled. However how do I bind the Collection to the ItemsControl?
Do I set the DataContext of the UserControl to RefreshTiles?
this.DataContext = RefreshTiles
then in XAML
<ItemsControl Name ="BoardControl" ItemsSource="{Binding}">
Update:
So apparently if I set the bind up in the MainWindow it works perfectly however this does not work when i try binding from a Usercontrol? I set a breakpoint in "RefreshArray" and I can see it being populated, however the UserControl does not update?
public partial class UserControl1 : UserControl
{
public char GetIteration
{
get { return MainWindow.Iteration; }
set { MainWindow.Iteration = value; }
}
CellCollection NewCells = new CellCollection();
public UserControl1()
{
InitializeComponent();
this.DataContext = NewCells;
PopulateCells();
}
private void PopulateCells()
{
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell NewCell = new Cell();
NewCell.Character = "A";
NewCell.Pos_x = i;
NewCell.Pos_y = j;
NewCells.Add(NewCell);
}
}
}
public void RefreshArray()
{
NewCells.Clear();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell ReCell = new Cell();
ReCell.Character = GetIteration.ToString();
ReCell.Pos_x = i;
ReCell.Pos_y = j;
NewCells.Add(ReCell);
}
}
this.DataContext = NewCells;
}
}
public partial class MainWindow : Window
{
UserControl1 Control = new UserControl1();
public static char Iteration = new char();
public MainWindow()
{
InitializeComponent();
}
private void Next_Click(object sender, RoutedEventArgs e)
{
Iteration = 'B';
Control.RefreshArray();
}
}
This doesn't work while the one below does work
public partial class MainWindow : Window
{
char Iteration = new char();
CellCollection NewCells = new CellCollection();
public MainWindow()
{
InitializeComponent();
PopulateCells();
this.DataContext = NewCells;
Iteration++;
}
private void PopulateCells()
{
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell NewCell = new Cell();
NewCell.Character = "A";
NewCell.Pos_x = i;
NewCell.Pos_y = j;
NewCells.Add(NewCell);
}
}
}
private void RefreshArray()
{
NewCells.Clear();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
Cell ReCell = new Cell();
ReCell.Character = Iteration;
ReCell.Pos_x = i;
ReCell.Pos_y = j;
NewCells.Add(ReCell);
}
}
}
private void Next_Click(object sender, RoutedEventArgs e)
{
RefreshArray();
}
}
There is no short answer to this question, so I'll give an outline feel free to ask more questions to get more details where needed:
To use databinding make sure the items in the collection implement INotifyPropertyChanged. Currently you are using an array of strings. String do not implement INotifyPropertyChanged.
Also make sure the collection implements INotifyCollectionChanged. Currently you are using an two dimensional array. An array does not implement this interface.
A solution would be to create a class named Tile that implements INotifyPropertyChanged and stores the character as a string and additionally stores its position on the board in an X and Y property:
public class Tile : INotifyPropertyChanged
{
private string character;
public string Character
{
get
{
return character;
}
set
{
if(character != value)
{
character = value;
OnPropertyChanged("Character");
}
}
}
private int x; // repeat for y and Y
public int X
{
get
{
return x;
}
set
{
if(x != value)
{
x = value;
OnPropertyChanged("X");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var p = PropertyChanged;
if(p != null)
{
p(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Create a class Tiles like this:
public class Tiles : ObservableCollection<Tile>
{
}
and replace the two dimensional array with this collection.
One way of binding an items control to the collection requires you to set the DataContext of the items control to an instance of the collection and specifying the property ItemsSource="{Binding}"
Use the Character, X and Y properties in the itemtemplate to display the text and position the tile.
This way the bindings will automagically update the view when you manipulate the Tiles collection when adding or removing tiles and the board will also be updated when a tile changes (either position or content)
Emo's approach is a good one; I'd suggest a slightly different tack.
First, set your existing program aside. You'll come back to it later.
Next, implement a prototype WPF project. In this project, create a class that exposes Letter, Row, and Column properties, and another class that exposes a collection of these objects. Write a method that fills this collection with test data.
In your main window, implement an ItemsControl to present this collection. This control needs four things:
Its ItemsPanel must contain a template for the panel it's going to use to arrange the items it contains. In this case, you'll be using a Grid with rows and columns of a predefined size.
Its ItemContainerStyle must contain setters that tell the ContentPresenter objects that the template generates which row and column of the grid they belong in.
Its ItemTemplate must contain a template that tells it what controls it should put in the ContentPresenters.
Its ItemsSource must be bound to the collection of objects.
A minimal version looks like this:
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.Row" Value="{Binding Row}"/>
<Setter Property="Grid.Column" Value="{Binding Column}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate TargetType="{x:Type MyClass}">
<TextBlock Text="{Binding Letter}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Set the DataContext of the main window to a populated instance of your collection, and you should see it lay the letters out in a 4x4 grid.
How this works: By setting ItemsSource to {Binding}, you're telling the ItemsControl to get its items from the DataContext. (Controls inherit their DataContext from their parent, so setting it on the window makes it available to the ItemsControl).
When WPF renders an ItemsControl, it creates a panel using the ItemsPanelTemplate and populates it with items. To do this, it goes through the items in the ItemsSource and, for each, generates a ContentPresenter control.
It populates the Content property of that control using the template found in the ItemTemplate property.
It sets properties on the ContentPresenter using the style found in the ItemContainerStyle property. In this instance, the style sets the Grid.Row and Grid.Column attached properties, which tell the Grid where to put them when it draws them on the screen.
So the actual objects that get created look like this:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Row="0" Grid.Column="0">
<ContentPresenter.Content>
<TextBlock Text="A"/>
</ContentPresenter.Content>
</ContentPresenter>
<ContentPresenter Grid.Row="1" Grid.Column="1">
<ContentPresenter.Content>
<TextBlock Text="A"/>
</ContentPresenter.Content>
</ContentPresenter>
</Grid>
(No actual XAML gets created, but the above XAML is a pretty good representation of the objects that do.)
Once you have this working, you now have a bunch of relatively straightforward problems to solve:
How do you make this look more like what you want it to look like? The answer to this is going to involve making a more elaborate ItemTemplate.
How do you sync the collection that this is presenting up with the array in your application? This depends (a lot) on how your application is designed; one approach is wrapping the collection in a class, having the class create an instance of your back-end object model, and having the back-end object model raise an event every time its collection changes, so that the class containing the collection of objects that are being presented in the UI knows to create a new front-end object and add it to its collection.
How does the user select a cell in the grid to put a tile into? The answer to this is probably going to involve creating objects for all the cells, not just the ones that contain letters, implementing a command to gets executed when the user clicks on the cell, and changing the ItemTemplate so that it can executes this command when the user clicks on it.
If the contents of a cell change, how does the UI find out about it? This is going to require implementing INotifyPropertyChanged in your UI object class, and raising PropertyChanged when Letter changes.
The most important thing about the approach I'm recommending here, if you haven't noticed, is that you can actually get the UI working almost completely independently of what you're doing in the back end. The UI and the back end are coupled together only in that first class you created, the one with the Row, Column, and Letter properties.
This, by the way, is a pretty good example of the Model/View/ViewModel pattern that you've probably heard about if you're interested in WPF. The code you've written is the model. The window is the view. And that class with the Row, Column, and Letter properties is the view model.

Categories

Resources