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.
Related
I've been searching for a while and I just cannot find what I'm doing wrong.
I have a list of names that I show in a View, in my View I created an itemsControl, the ItemsSource is set to the observableCollection in the ViewModel. The goal is to give a overview of available names in an nxn table. The user should be able to filter the results with the searchbox on top.
First I tried this using a List of Lists which did not work as the view was not being updated according to the string in the searchbox. I found that I should actually use a ObservableCollection because it implements INotifyCollectionChanged. Below I try to implement an ObservableCollection but I fail to update the view when this collection changes.
View (see ItemsControl in the Xaml section):
Xaml Resources
<UserControl.Resources>
<DataTemplate x:Key="DataTemplate_Level2">
<Button Content="{Binding}" Height="150" Width="150" Margin="20,20,20,20">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#2ED99A"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2EB9D9"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#333f4a"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_Level1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_Level2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</UserControl.Resources>
Xaml
<Grid Margin="10,10,10,10" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
<TextBlock Text="{Binding HcfOverviewModel.NSharedModules, StringFormat=Shared Circuits: {0}}"></TextBlock>
<TextBlock Margin="30,0,0,30" Text="{Binding HcfOverviewModel.NPrivateModules, StringFormat=Private Circuits: {0}}"></TextBlock>
</StackPanel>
<Button Grid.Column="1" VerticalAlignment="Top" MaxHeight="20" Content="See Private Circuits"></Button>
</Grid>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
<ItemsControl HorizontalAlignment="Center" VerticalAlignment="Top" ItemsSource="{Binding Modules, UpdateSourceTrigger=PropertyChanged}" ItemTemplate="{DynamicResource DataTemplate_Level1}"/>
</ScrollViewer>
</Grid>
ViewModel:
class HcfOverviewViewModel:BaseViewModel
{
private HcfOverviewModel _hcfOverviewModel;
private string _pathToSharedModules;
//private List<List<string>> _modules;
private ObservableCollection<ObservableCollection<string>> _modules;
public HcfOverviewViewModel()
{
_pathToSharedModules = #"C:\Users\scamphyn\source\repos\TSD\TSD\TempTestFolder";
_hcfOverviewModel = new HcfOverviewModel();
_hcfOverviewModel.NSharedModules = CheckDirectory.FindModules(_pathToSharedModules).Length;
_hcfOverviewModel.SharedModuleNames = CleanModuleNames(CheckDirectory.FindModules(_pathToSharedModules));
//_modules = CreateGridLayout(_hcfOverviewModel.SharedModuleNames);
_modules = CreateGrid(_hcfOverviewModel.SharedModuleNames);
}
public HcfOverviewModel HcfOverviewModel
{
get => _hcfOverviewModel;
}
public void Filter(string searchString)
{
Console.WriteLine(searchString);
if (searchString == "")
{
//Modules = CreateGridLayout(_hcfOverviewModel.SharedModuleNames);
Modules = CreateGrid(_hcfOverviewModel.SharedModuleNames);
}
else
{
Modules.Clear();
string[] filteredNames = _hcfOverviewModel.SharedModuleNames.Where(n => n.Contains(searchString)).ToArray();
//Modules = CreateGridLayout(filteredNames);
Modules = CreateGrid(filteredNames);
}
}
private string[] CleanModuleNames(string[] listToClean)
{
List<string> moduleNames = new List<string>();
foreach (string s in listToClean)
{
string[] listPath = s.Split('\\');
string name = listPath[listPath.Length - 1];
moduleNames.Add(name);
}
return moduleNames.ToArray();
}
public ObservableCollection<ObservableCollection<string>> CreateGrid(string[] moduleNames)
{
ObservableCollection<ObservableCollection<string>> lsts = new ObservableCollection<ObservableCollection<string>>();
int circuitsPerRow = 4;
int _nRows = moduleNames.Length / circuitsPerRow;
Queue<string> modules = new Queue<string>(moduleNames);
for(int i = 0; i <= _nRows; i++)
{
lsts.Add(new ObservableCollection<string>());
for (int j = 0; j < circuitsPerRow; j++)
{
if (modules.Count != 0)
{
lsts[i].Add(modules.Dequeue());
}
else
{
break;
}
}
}
return lsts;
}
public List<List<string>> CreateGridLayout(string[] moduleNames)
//To Create the grid in HcfOverview
{
List<List<string>> lsts = new List<List<string>>(); // create list of list to store nxn matrix data in.
int circuitsPerRow = 4;
//Determine correct size nxn
int _nRows = moduleNames.Length / circuitsPerRow;
//Create Queue
Queue<string> modules = new Queue<string>(moduleNames);
//Create data for ItemControls
for (int i = 0; i <= _nRows; i++) // number of rows
{
lsts.Add(new List<string>());
for (int j = 0; j < circuitsPerRow; j++) // number of columns
{
if (modules.Count != 0)
{
lsts[i].Add(modules.Dequeue());
}
else {
break;
}
}
}
return lsts;
}
public ObservableCollection<ObservableCollection<string>> Modules
{
get => _modules;
set {
_modules = value;
OnPropertyChanged("Modules");
}
}
}
"_modules" which is my ObservableCollection is populated in the CreateGrid Method.
Here is what it looks like in the window:
The search box is in the "parent" View/ViewModel when I detect an update there I call the filter method in my ViewModel shown above to update the ObservableCollection.
How can this be resoloved?
Update:
Because I've been stuck on this issue for week now I decided to create a new "Project" and check if I could get it to work in a simplified project.
In this simplified project I managed to add and remove rows. I used the exact same methods as I was using before. So the binding and updating of my observablecollection is done right. I now wonder if this issue would be in how I load this UserControl. The UserControl displaying this data is loaded through another View. see code below:
<ContentControl Grid.Row="2" Content="{Binding CurrentHcfViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type ViewModels:HcfOverviewViewModel}">
<views:UserControlHcfOverview/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
My HcfOverviewViewModel is loaded into a usercontrol that exists in a different view. Could this have influence on the updating of the ObeservableCollection and its View in the HcfOverviewViewModel?
I found that I should actually use a ObservableCollection because it implements INotifyCollectionChanged.
Due to how you do your update its not working as you believe. When one changes the reference from one ObservableCollection to another ObservableCollection, that does not send a INotifyCollectionChanged message. Only add/delete/clear calls generate that message on an active referenced collection.
When you reset the reference you are telling the ItemsControl to fully clear itself and then show what is being cleared.
You need to not change the initial Modules collection after it is created. There are other issues with your logic that cascade from that and unless someone rewrites your custom control/logic, this question cannot be fully answered.
Regardless you need to start with not changing the references and then work on the changing internal collection items in the same pattern to the parent ObservableCollection.
Note, some controls need a Null to be set when one is setting to a new list. Its unclear if that is also compounding the issue, for you immediately change to the newer reference and the new reference top level does not seem to change the whole ItemsControl.
I have a WPF Grid which is 3 columns wide and 8 rows:
<Window x:Class="Container.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="700" Width="1000">
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="10*" />
</Grid.ColumnDefinitions>
</Grid>
</Window>
I am using this to draw something like this:
Every cell in the first and third columns may have a different number of rectangles. Also, the width of each rectangle may be different and change at run-time. The width will be proportionate to a number (known at run-time and continually-changing).
What is the best way to draw these rectangles?
Here is what I've come up with after about an hour of fiddling (GitHub Repo):
I'm using the MVVM pattern to make the UI as easy as possible. Right now, it just populates with some random data.
The XAML:
<Window
x:Class="BuySellOrders.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BuySellOrders"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainWindowVm />
</Window.DataContext>
<Grid Margin="15">
<ItemsControl ItemsSource="{Binding Path=Prices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:PriceEntryVm}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Padding="5"
HorizontalAlignment="Stretch"
BorderBrush="Black"
BorderThickness="1">
<ItemsControl HorizontalAlignment="Right" ItemsSource="{Binding Path=BuyOrders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OrderVm}">
<Border
Width="{Binding Path=Qty}"
Margin="5"
Background="red"
BorderBrush="Black"
BorderThickness="1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<Border
Grid.Column="1"
BorderBrush="Black"
BorderThickness="1">
<TextBlock
Margin="8"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Path=Price}" />
</Border>
<Border
Grid.Column="2"
Padding="5"
BorderBrush="Black"
BorderThickness="1">
<ItemsControl ItemsSource="{Binding Path=SellOrders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OrderVm}">
<Border
Width="{Binding Path=Qty}"
Margin="5"
Background="red"
BorderBrush="Black"
BorderThickness="1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
The view models:
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
var rnd = new Random();
Prices = new ObservableCollection<PriceEntryVm>();
for (int i = 0; i < 8; i++)
{
var entry = new PriceEntryVm();
Prices.Add(entry);
entry.BuyOrders.CollectionChanged += OnOrderChanged;
entry.SellOrders.CollectionChanged += OnOrderChanged;
entry.Price = (decimal)110.91 + (decimal)i / 100;
var numBuy = rnd.Next(5);
for (int orderIndex = 0; orderIndex < numBuy; orderIndex++)
{
var order = new OrderVm();
order.Qty = rnd.Next(70) + 5;
entry.BuyOrders.Add(order);
}
var numSell = rnd.Next(5);
for (int orderIOndex = 0; orderIOndex < numSell; orderIOndex++)
{
var order = new OrderVm();
order.Qty = rnd.Next(70) + 5;
entry.SellOrders.Add(order);
}
}
}
private void OnOrderChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var order = item as OrderVm;
if (order.Qty > LargestOrder)
{
LargestOrder = order.Qty;
}
}
}
}
private int _largestOrder;
public int LargestOrder
{
get { return _largestOrder; }
private set { SetValue(ref _largestOrder, value); }
}
public ObservableCollection<PriceEntryVm> Prices { get; }
}
public class PriceEntryVm: ViewModel
{
public PriceEntryVm()
{
BuyOrders = new OrderList(this);
SellOrders = new OrderList(this);
}
private Decimal _price;
public Decimal Price
{
get {return _price;}
set {SetValue(ref _price, value);}
}
public OrderList BuyOrders { get; }
public OrderList SellOrders { get; }
}
public class OrderList : ObservableCollection<OrderVm>
{
public OrderList(PriceEntryVm priceEntry)
{
PriceEntry = priceEntry;
}
public PriceEntryVm PriceEntry { get; }
}
public class OrderVm : ViewModel
{
private int _qty;
public int Qty
{
get { return _qty; }
set { SetValue(ref _qty, value); }
}
}
I had to make some assumptions about the naming of things, but hopefully you should get the basic idea of what's going on.
It's structured as a list of PriceEntry, each of which contains a Price, and a BuyOrders and SellOrders properties.
BuyOrders and SellOrders are just lists of orders that have a Quantity property.
The XAML binds the list of price entries to a template that contains a 3 column grid. The first and 3rd columns of that grid bound to another set of item controls for each list of orders. The template for each order is just a border with a Width bound to the Quantity of the order.
All the binds means that just updating a property, or adding an order to either the buy or sell list of a price entry will automatically propagate to the UI. Adding or removing a PriceEntry will also automatically adjust the UI.
I haven't implemented your automatic scaling yet, but the basic idea would be to use a ValueConverter on the Quantity binding, to make it automatically adjust to the largest order.
As an extra note, it uses this nuget package to provide some of the MVVM boiler-plate code, but you should be able to use anything you want, as long as it gives you INotifyPropertyChanged support.
Here is a bonus screen capture showing the dynamic nature of MVVM updating the UI based on a timer.
This only needed a few lines of code to randomly pick a row, then randomly pick an order on the row, then add or subtract a small random amount from the quantity.
_updateTimer = new DispatcherTimer();
_updateTimer.Tick += OnUpdate;
_updateTimer.Interval = TimeSpan.FromSeconds(0.01);
_updateTimer.Start();
private void OnUpdate(object sender, EventArgs e)
{
var entryIndex = _rnd.Next(Prices.Count);
var entry = Prices[entryIndex];
OrderList list;
list = _rnd.Next(2) == 1 ?
entry.BuyOrders :
entry.SellOrders;
if (list.Any())
{
var order = list[_rnd.Next(list.Count)];
order.Qty += _rnd.Next(0, 8) - 4;
}
}
Right then, here goes....
This is exactly the kind of thing you want to use data-binding for. You can try and do things manually if you like, but your code will quickly become very messy if you do. WPF lets you do things the old-school way (i.e. similar to WinForms et al) but that was really to facilitate porting of legacy code. I won't go into too much detail about MVVM (plenty of info on the net about it), but you can get started by using NuGet to add MVVMLightLibs or some other MVVM framework to your project and then you assign your main window a view model by doing something like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
So now it's time for the view model itself, which is a model of the data structures that you want your view to display:
public class MainViewModel : ViewModelBase
{
public ObservableCollection<PriceLevel> PriceLevels { get; } = new ObservableCollection<PriceLevel>
{
new PriceLevel(110.98, new int[]{ }, new int[]{ }),
new PriceLevel(110.97, new int[]{ }, new int[]{ }),
new PriceLevel(110.96, new int[]{ }, new int[]{ }),
new PriceLevel(110.95, new int[]{ }, new int[]{ 5 }),
new PriceLevel(110.94, new int[]{ }, new int[]{ 3, 8 }),
new PriceLevel(110.93, new int[]{ 8, 3, 5, }, new int[]{ }),
new PriceLevel(110.92, new int[]{ 3 }, new int[]{ }),
new PriceLevel(110.91, new int[]{ }, new int[]{ }),
};
}
public class PriceLevel
{
public double Price { get; }
public ObservableCollection<int> BuyOrders { get; }
public ObservableCollection<int> SellOrders { get; }
public PriceLevel(double price, IEnumerable<int> buyOrders, IEnumerable<int> sellOrders)
{
this.Price = price;
this.BuyOrders = new ObservableCollection<int>(buyOrders);
this.SellOrders = new ObservableCollection<int>(sellOrders);
}
}
If you don't already know, ObservableCollection is very similar to list but it propegrates change notification, so when you make your view display the data in it your GUI will update automatically whenever the list changes. This MainViewModel class contains an ObservableCollection of type PriceLevel, and each PriceLevel contains the price and the lists of buy and sell orders. This means you'll be able to add and remove price points, and also add and remove the orders in the price points, and your front-end will reflect those changes.
So on to the front end itself:
<Window.Resources>
<!-- Style to display order list as horizontal list of red rectangles -->
<Style x:Key="OrderListStyle" TargetType="{x:Type ItemsControl}">
<!-- Set ItemsPanel to a horizontal StackPanel -->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!-- Display each item in the order list as a red rectangle and scale x by 8*size -->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Margin="5" >
<Rectangle Width="{Binding}" Height="20" Fill="Red">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="8" ScaleY="1" />
</Rectangle.LayoutTransform>
</Rectangle>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Style to make Price cells vertically aligned -->
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid Background="{TemplateBinding Background}">
<ContentPresenter VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- This style centers the column's header text -->
<Style TargetType="DataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</Window.Resources>
<!-- This datagrid displays the main list of PriceLevels -->
<DataGrid ItemsSource="{Binding PriceLevels}" AutoGenerateColumns="False" IsReadOnly="True"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False"
CanUserResizeRows="False" CanUserSortColumns="False" RowHeight="30">
<DataGrid.Columns>
<!-- The buy orders column -->
<DataGridTemplateColumn Header="Buy orders" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding BuyOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Right" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- The price column -->
<DataGridTextColumn Header="Price" Width="Auto" Binding="{Binding Price}" />
<!-- The sell orders column -->
<DataGridTemplateColumn Header="Sell Orders" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding SellOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Left" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Looks a bit full-on, but if you break it down into sections it's actually pretty straightforward. The main different between this and what you've been trying to do is that I'm using a DataGrid. This is basically a Grid control that's had extra functionality added to make it respond dynamically to data that it's been bound to. It also has a lot of extra stuff we dont' need (editing, column resize/reordering etc) so I've turned all that off. The DataGrid binds to PriceLevels in the view model, so it will display a vertical list showing each one. I've then explicitly declared the 3 columns you're after. The middle one is easy, it's just text, so DataGridTextColumn will, do the job. The other two are horizontal arrays of rectangles, so I've used DataGridTemplateColumn which allows me to customize exactly how they look. This customization is mostly done in the OrderListStyle at the very top of the XAML which sets ItemsPanel to a horizontal StackPanel and sets ItemTemplate to a rectangle. There's also a bit of XAML in there to scale the rectangle by a constant, according to the value of the integer it's displaying in the order list.
Here's the result:
I know the XAML might seem a little full-on, but keep in mind this is now fully data-bound to that view model and it will automatically update in response to changes. This little bit of extra work at the start results in MUCH cleaner update code which is also easier to test and debug.
Hope this is what you're after, if you have any questions let me know and we can take it into chat.
UPDATE: If you want to see the dynamic update in action then add this to your main view model's constructor, it just adds and removes orders randomly:
public MainViewModel()
{
var rng = new Random();
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(0.1);
timer.Tick += (s, e) =>
{
var row = this.PriceLevels[rng.Next(this.PriceLevels.Count())]; // get random row
switch (rng.Next(4))
{
case 0: row.BuyOrders.Add(1 + rng.Next(5)); break;
case 1: row.SellOrders.Add(1 + rng.Next(5)); break;
case 2: if (row.BuyOrders.Count() > 0) row.BuyOrders.RemoveAt(rng.Next(row.BuyOrders.Count())); break;
case 3: if (row.SellOrders.Count() > 0) row.SellOrders.RemoveAt(rng.Next(row.SellOrders.Count())); break;
}
};
timer.Start();
}
I have an array that keeps changing its values, because of this I want to have the apps UI refreshing every time the array's values do. I have this bound with an itemsControl. I can show the first array's values but then I can't update them I have tried .items.Clear() but its not working. Here are snippets of the .xaml and the xaml.cs. I actually took the code of the .xaml from a question from this site.
.xaml
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox Text="Testing" IsReadOnly="True"></TextBox>
<ItemsControl x:Name="itemsControl"
ItemsSource="{Binding itemsControl}"
FontSize="24">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="Auto"
Margin="0 12"
HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Column="0"
Grid.Row="0"
Orientation="Horizontal">
<TextBlock Name="txtblk0" Text="{Binding}" />
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
.xaml.cs
String c = (new String(cArray));
string[] arr = null;
string[] data = null;
if (c != null)
{
arr = c.Split('\n');
if (arr.Length > 0)
{
data = arr[0].Split(',');
}
}
for(int index = 0; index < 4; index++)
{
itemsControl.Items.Add(float.Parse(data[index]));
}
itemsControl.Clear();
If anyone has an idea of how I can do this I will be very grateful, thanks in advance and I will try to answer any questions as soon as possible!
What you're missing is an understanding of how bindings are triggered to update.
The INotifyPropertyChanged interface contains a method (PropertyChanged) and when called and passed the name of a property will tell the binding system that the property has changed and the binding should be updated.
INotifyCollectionChanged is the equivalent for collections, and communicates when a collection has changed. i.e. something added, removed, or the list cleared.
ObservableCollection<T> contains an implementation of INotifyCollectionChanged that makes it easy to work with lists, collections, etc. that change.
If you used an ObservableCollection<float> instead of an array you'd be able to modify the list and have the UI updated to reflect this easily.
As a starter, see the following which demonstrates how easy it is to use an ObservableCollection.
XAML:
<StackPanel>
<Button Click="Button_Click">add an item</Button>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
code behind;
public MainPage()
{
this.InitializeComponent();
// Initialize the property
this.Items = new ObservableCollection<string>();
// Use self as datacontext (but would normally use a separate viewmodel)
this.DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// add a new item to the UI
this.Items.Add(DateTime.Now.ToString());
}
// The "collection" that is shown in the UI
public ObservableCollection<string> Items { get; set; }
Project:
I have a parent panel which holds a ComboBox and FlowLayoutPanel. The FlowLayoutPanel holds a variable number of child panels (a custom control that inherits from UserControl). Each child panel contains some labels, two ComboBoxes, a button, and a DataGridView with 3 ComboBox columns and a button column. The DataGridView may have 1-6 rows. The FlowLayoutPanel is populated with child panels when an item is selected from the ComboBox on the parent panel.
Problem:
Populating the FlowLayoutPanel with about 50 child panels takes about 2.5 seconds. Specifically, I've determined that the call to FlowLayoutPanel.Controls.AddRange() is the culprit.
Relevant Code: I can't post all of my code here (too much code plus parts of it are confidential), but I'll do my best to explain what is happening.
Parent Panel:
private void displayInformation(Suite suite)
{
this.SuspendLayout();
// Get dependencies.
List<SuiteRange> dependents = new List<SuiteRange>(suite.dependencies.Keys);
dependents.Sort(SuiteRange.Compare);
// Create a ChildPanel for each dependent.
List<ChildPanel> rangePanels = new List<ChildPanel>();
foreach (SuiteRange dependent in dependents)
{
ChildPanel sdp = new ChildPanel();
sdp.initialize(initialSuite.name, dataAccess);
sdp.displayInformation(dependent, suite.dependencies[dependent]);
rangePanels.Add(sdp);
}
// Put the child panels in the FlowLayoutPanel.
flpDependencyGroups.SuspendLayout();
// Takes ~2.5 seconds
flpDependencyGroups.Controls.AddRange(rangePanels.ToArray());
flpDependencyGroups.ResumeLayout();
// Takes ~0.5 seconds
updateChildPanelSizes();
this.ResumeLayout();
}
Things I've tried:
Call SuspendLayout() / ResumeLayout() on the parent panel and/or FlowLayoutPanel. Minimal performance increase (~0.2 seconds).
Use Control.FlatStyle.Flat on ComboBoxes, Buttons, and DataGridView columns. Minimal performance increase (~0.1 seconds).
Verified that none of my controls use a transparent background color.
Set ChildPanel.DoubleBuffered and ParentPanel.DoubleBuffered to true.
Remove the FlowLayoutPanel from its parent before calling AddRange() and re-adding it after.
Things that might be relevant:
The panels and controls use anchors (as opposed to autosize or dock).
My controls are manually populated and do not use the DataSource property.
EDIT: Solution:
#HighCore's answer is the correct solution. Unfortunately I won't be implementing it at this time (it could happen down the road) because I found a workaround. The workaround doesn't really solve the problem, just masks it, hence why I'm not posting this as an answer. I discovered that the form loads in half the time if the Dependencies tab isn't on top (i.e. the Product Lists tab is selected). This reduces loading time to about 1 second, which is acceptable. When data is being loaded and the Dependencies tab is on top, I switch to the Product Lists tab, throw up a dark grey box over the tab control that says "Loading..." in the middle, load the data, and then switch back to the Dependencies tab.
Thanks all for your comments and suggestions, it was greatly appreciated.
Posting this answer because the OP requested it:
This is how you'd do something like that in WPF:
<UserControl x:Class="WpfApplication7.ListBoxSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<Button Content="Load" Click="Load_Click" DockPanel.Dock="Top"/>
<ListBox ItemsSource="{Binding}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" Padding="5"
Background="#FFFAFAFA">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Dependent Versions" FontWeight="Bold"
Grid.ColumnSpan="2" HorizontalAlignment="Center"/>
<TextBlock Text="From:" FontWeight="Bold"
Grid.Row="1" HorizontalAlignment="Center"/>
<TextBlock Text="To (exclusive):" FontWeight="Bold"
Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center"/>
<ComboBox SelectedItem="{Binding From}"
ItemsSource="{Binding FromOptions}"
Grid.Row="2" Margin="5"/>
<ComboBox SelectedItem="{Binding To}"
ItemsSource="{Binding ToOptions}"
Grid.Row="2" Grid.Column="1" Margin="5"/>
<DataGrid ItemsSource="{Binding ChildItems}"
AutoGenerateColumns="False" CanUserAddRows="False"
Grid.Column="2" Grid.RowSpan="4">
<DataGrid.Columns>
<DataGridTextColumn Header="XXXX" Binding="{Binding XXXX}"/>
<DataGridTextColumn Header="Dependee From" Binding="{Binding DependeeFrom}"/>
<DataGridTextColumn Header="Dependee To" Binding="{Binding DependeeTo}"/>
<DataGridTemplateColumn Width="25">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="X"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Delete"
Grid.Column="3"
HorizontalAlignment="Right" VerticalAlignment="Top"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>
Code Behind (only boilerplate to support the example)
public partial class ListBoxSample : UserControl
{
public ListBoxSample()
{
InitializeComponent();
}
public void LoadData()
{
Task.Factory.StartNew(() =>
{
var list = new List<DataItem>();
for (int i = 0; i < 100000; i++)
{
var item = new DataItem()
{
From = "1",
To = "2",
ChildItems =
{
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
},
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
},
new ChildItem()
{
DependeeFrom = i.ToString(),
DependeeTo = (i + 10).ToString(),
XXXX = "XXXX"
}
}
};
list.Add(item);
}
return list;
}).ContinueWith(t =>
{
Dispatcher.Invoke((Action) (() => DataContext = t.Result));
});
}
private void Load_Click(object sender, System.Windows.RoutedEventArgs e)
{
LoadData();
}
}
Data Items:
public class DataItem
{
public List<ChildItem> ChildItems { get; set; }
public List<string> FromOptions { get; set; }
public List<string> ToOptions { get; set; }
public string From { get; set; }
public string To { get; set; }
public DataItem()
{
ChildItems = new List<ChildItem>();
FromOptions = Enumerable.Range(0,10).Select(x => x.ToString()).ToList();
ToOptions = Enumerable.Range(0, 10).Select(x => x.ToString()).ToList();
}
}
public class ChildItem
{
public string XXXX { get; set; }
public string DependeeFrom { get; set; }
public string DependeeTo { get; set; }
}
Then you put that in an existing winforms UI using an ElementHost:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var elementHost = new ElementHost
{
Dock = DockStyle.Fill,
Child = new ListBoxSample()
};
Controls.Add(elementHost);
}
}
Result:
Notice that I added 100,000 records. Still, response time (both when scrolling and interacting with the UI) is immediate due to WPF's built in UI Virtualization.
Also notice that I'm using DataBinding which removes the need to manipulate UI elements in procedural code. This is important because the WPF Visual Tree is a complex structure, and DataBinding is the preferred approach in WPF always.
Also notice by resizing the form that the UI is completely resolution independent. You can customize it further by making the ComboBoxes fixed and having the DataGrid stretch to the remaining space. See WPF Layouts.
WPF Rocks. - see how much you can achieve with so little code, and without spending lots of $$$ in third party controls. You should really forget winforms forever.
You will need to target .Net 3.0 at a minimum, but 4.0/4.5 is highly recommended because WPF had several issues in earlier versions, which were fixed in 4.0.
Make sure you reference PresentationCore.dll, PresentationFramework.dll, WindowsBase.dll, System.Xaml.dll and WindowsFormsIntegration.dll, all of which belong to the .Net Framework itself (no 3rd parties)
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);