So I have this idea of adding several buttons to a grid, then arranging them, and the number of buttons added is related to a number I enter. All works fine, until I try to make it work under MVVM.
Code that works in the MainWindow.xaml.cs:
private void Button_Click(object sender, RoutedEventArgs e)
{
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width=50;
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background=bs[0];
}
else btn.Background = bs[1];
GameGrid.Children.Add(btn);
}
}
}
So basically, I press a button and it adds 3x3 buttons, colored nicely and spread appropriately. But then I get to the MVVM:
private void ButtonClickCommand()
{
RowCount = GridNumber;
ColumnCount=GridNumber;
int i = 0, j = 0;
Brush[] bs = { Brushes.BurlyWood, Brushes.Honeydew };
for (i = 0; i < GridNumber; i++)
{
for (j = 0; j < GridNumber; j++)
{
Button btn = new Button();
btn.Height = 50;
btn.Width = 50;
// btn.Command = StartCommand; Ill add that later
btn.SetValue(Grid.RowProperty, i);
btn.SetValue(Grid.ColumnProperty, j);
if ((i + j) % 2 == 0)
{
btn.Background = bs[0];
}
else btn.Background = bs[1];
somebuttonlist.Add(btn);
}
}
}
Here I have a List of buttons, which should accept the new created buttons, then transfer them to the grid. Code of the button list:
private List<Button> _bslist = new List<Button>();
public List<Button> somebuttonlist
{
get
{
return _bslist;
}
set
{
_bslist = value;
NotifyPropertyChanged();
}
}
And xaml code:
<ItemsControl ItemsSource="{Binding somebuttonlist}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Name="GameGrid" Visibility="{Binding GameVis}"
vm:GridHelpers.RowCount="{Binding RowCount}"
vm:GridHelpers.ColumnCount="{Binding ColumnCount}">
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Don't mind all the bindings, they work properly when I use the non-MVVM format.
So how do I properly transfer the working code into the View Model? Is there a way to access the children accordingly?
It looks like you are binding to a List, which does not notify when new records are inserted or any record is deleted. The NotifyPropertyChanged() that you have only kicks off when the reference to that list is modified, so for example, setting it to a new List() or a different List. Try changing it to an ObservableCollection instead, which sends an event for when the number of records in the collection get modified.
This is not the right way of doing things with MVVM. What you are trying to do now is to pull visual code into your ViewModel. Ideally, ViewModels should leave in a separate project without any references to visual stuff. This way, referencing Button will result in broken code unless you add a reference for System.Windows.Controls and this reminds you that you do things the wrong way.
What you are trying to do is doable in pure MVVM way, but will result in much more code, which also be convoluted to you if you haven't got a grasp on MVVM yet. Also note, that in real life, it is acceptable to have some code behind for complicated scenarios. For this you just create a custom control to encapsulate all code behind logic.
The nice solution in your case can be a custom control, that encapsulates Grid and logic to populate it with buttons. You also add dependency properties for Rows and Columns, so you can bind to them in XAML, and define a command which is invoked when button is clicked. This may be a good example: How to create bindable commands in Custom control?
Related
I've been working on drawing a grid in C#, which works! But somehow I keep running into some problems with the method DrawGrid.
I found that it is being redrawn every time a label gets added, and continuously after that. Here is my code:
Rectangle[,] rec = new Rectangle[6,6];
Label label_1 = new Label();
Label label_2 = new Label();
public Reversi()
{
ClientSize = new Size(500,425);
BackColor = Color.DarkGreen;
NewGame();
Paint += DrawGrid;
}
public void NewGame()
{
// here is some more stuff which I will leave out for the sake of clarity
for (int i = 0; i < grspel; i++)
{
for (int j = 0; j < grspel; j++)
{
rec[i, j] = new Rectangle(50 + i * 50, 100 + j * 50, 50, 50);
}
}
DrawLabels();
}
public void DrawLabels()
{
label_1.Location = new Point(20, 50);
label_1.Text = "Zwart: " + zwart;
Controls.Add(label_1);
label_2.Location = new Point(330, 50);
label_2.Text = "Wit: " + wit;
Controls.Add(label_2);
}
public void DrawGrid(object o, PaintEventArgs pea)
{
MessageBox.Show("test");
for (int i = 0; i < 6; i++)
{
for (int j = 0; j < 6; j++)
{
pea.Graphics.DrawRectangle(new Pen(Brushes.Black), rec[i,j]);
}
}
}
To visualize my problem I placed MessageBox.Show("test"); at the start of my DrawGrid method. The rest of my code (which is not on here) uses this method and is not working properly. I've narrowed the problem down to this.
Getting rid of the Controls.Add(label_i) fixes the problem, but I need the labels.
Why does this happen and more importantly, how can I fix it?
You add a label, thus there's an event that UI changes and needs to repaint. It should paint the added label, right? IMHO that's expected.
You don't need to add the labels to the list of controls each time. Just add them once (e.g. in Form.Load).
However, changing the position of the label will likely also require a repaint. It should paint the label at the new position, right? But your position is constant as well. Just set the position once.
Repainting is normal and good, because it keeps the contents updated. It shouldn't be a problem. So, why don't you like it? Probably it causes flickering and you don't like that.
Showing a MessageBox might not be a good idea either. If the MessageBox is in front of your form, the part that is behind the MessageBox will need to be painted again.
You can
use SuspendLayout() to tell the painter that it's not worth reacting to changes immediately, but wait until you finished making all layout related changes. Use ResumeLayout() to tell the painter it's time to paint now.
check out options for double buffering
but I need the labels
Actually you don't. You can use Graphics.DrawString() instead of using a label.
I read a lot of topics about usage of ObservableCollection, but I don't understand what should I do in my case. I have the following PageContent structure:
I use IList collection to store my Xamarin.Form.Image. I take it from the database. The number of the grid layout rows and columns definitely depend on the number of images in a database. User can load new images to a database. It ads to a database by using ImageRepository and it ads to the IList<T> collection, but I don't know how to dynamically redraw my grid layout. I tried to do this as follows:
//this.images
//is a IList<Images> which stores as property of the class.
private void OnClickSearchImagesButton(object sender, EventArgs args)
{
IFiler filer = DependencyService.Get<IFiler>();
// Get all paths of the images
IEnumerable<string> filesPath = filer.GetFilesPaths(FileExtension.JPG);
IEnumerable<Image> images = filesPath.Select(f => new Image { Source = ImageSource.FromFile(f) });
foreach (Image img in images)
{
// If found image does not exist
// Add it to a database
// --------------------
// Redraw grid layout
this.gridLayout = new Grid();
const int numberOfImagesPerRow = 3;
int numberOfImages = this.images.Count;
int numberOfRows = (int)Math.Ceiling((double)numberOfImages / numberOfImagesPerRow);
// configure grid layout
for (int i = 0; i < numberOfRows; i++)
{
this.gridLayout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(200) });
}
for (int i = 0; i < numberOfImagesPerRow; i++)
{
this.gridLayout.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
for (int i = 0, imageNumber = 0; i < numberOfRows; i++)
{
for (int j = 0; j < numberOfImagesPerRow && imageNumber < numberOfImages; j++, imageNumber++)
{
this.gridLayout.Children.Add(images[imageNumber], j, i);
}
}
}
}
But it didn't help me. I saw some topics when people use ObservableCollection for items instead of List. I'm a little bit confused with it, because I think that I need to store all my items in ObservableCollection. Please, could you explain me what is the best way to hold actual information on the page?
ObservableCollection is very useful when you use controls like ListView. If you add a record or remove a record from the ObservableCollection, ListView updates automatically.
If you create a Grid and add Images to it, you should add or remove items "by code". ObservableCollection can't help you.
If you need a ListView that display images (so you can use ObservableCollection) you can take a look to
FlowListView
it's a powerful plugin that can visualize Images in a List
DataTemplate and DataTemplateSelector support
Fixed or automatic column count
Grouping support
Columns can expand to empty space (configurable)
ANY View can be used as a cell
All Xamarin.Forms platforms supported
Simple sample
<flv:FlowListView FlowColumnCount="3" SeparatorVisibility="None" HasUnevenRows="false"
FlowItemTappedCommand="{Binding ItemTappedCommand}" FlowLastTappedItem="{Binding LastTappedItem}"
FlowItemsSource="{Binding Items}" >
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Label HorizontalOptions="Fill" VerticalOptions="Fill"
XAlign="Center" YAlign="Center" Text="{Binding Title}"/>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
If you need a demo
DEMO
ObservableCollection is helpful because it fires events when the collection changes, but you need to either use a control that consumes those events, or create one yourself. In your case, I would suggest looking at some third party controls that layout images as you want (another answer mentioned FlowListView), or look at creating a custom layout in Xamarin Forms.
I am under a situation where i have to click the button which are created dynamically with programming in c# (because user decides at run time how many button to be created).
I do so by writing code like this:
int runTimeIntegerValue = 6; //Lets say its 6
Button[] childGridSavebutn = new Button[runTimeIntegerValue -1];
for (int j = 0; j < runTimeIntegerValue; j++)
{
childGridSavebutn[j] = GenerateButton("Save Table");
childSmallgrid[j] = generateRowColumnGrid(1, 1);
Grid.SetColumn(childGridSavebutn[j], 1);
Grid.SetRow(childGridSavebutn[j], i);
childSmallgrid[j].Children.Add(childGridSavebutn[j]);
childGridSavebutn[j].Click += (source, e) =>
{
txtBoxOrder[j].Text = "I am from " + j;
MessageBox.Show("I am from " + j); //This message always show "I am From 5"
};
}
private Button GenerateButton(string p)
{
Button btn = new Button();
//btn.Name = p;
btn.Content = p;
btn.Width = 80;
btn.HorizontalAlignment = HorizontalAlignment.Center;
btn.Height = 20;
return btn;
}
After I popup a message MessageBox.Show("I am from " + i); to know the message box is popuped from which button . It always popups 5 (as last count of i is 5).
So How to know which button is clicked, by click on that button? As there are 5 buttons, so i should know by click on the button that button number 1/2/3/4/5 is clocked by pop up of message with respected i position.
(I mean button at ith position is clicked, and message must show this the "I am from 2" when 2nd button is clicked)
SUMMARY: I have childGridSavebutn[1 to 5] array (it could be even 10 decided run time by user). And it display 5 buttons, How to know which button is clicked ? (as it always popups 5th one) because further i have to generate button click events receptively. How will i do that if i do not know which button is clicked. At last UI has to repeat like this 5 times
Are you sure you need to create these buttons programatically? Consider approach of generating buttons purely in XAML basing on your view models.
Suppose you hava a view model for single button:
public class SingleButtonViewModel {
public string Text { get; set; }
public ICommand Command { get; set; }
}
Your parent view model:
public class ParentViewModel {
public IList<SingleButtonViewModel> Buttons { get; set; }
}
And in your XAML:
<ItemsControl ItemsSource="{Binding Buttons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Text}" Command="{Binding Command}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Then you create your button view models programmaticaly, give it any command you want.
Consider, that I was not implementing INotifyPropertyChanged and so for the sake of readibility.
Try changing your code to.
for (int jj = 0; jj < runTimeIntegerValue; jj++)
{
int j = jj;
some code as you had before
}
The problem you are getting is due to how C# captured variables in closures.
Eric Lippert has an in-depth explanation of this behaviour:
Closing over the loop variable considered harmful
Closing over the loop variable, part two
Basically, it's the loop variable that is captured, not it's value.
Everyone hits this issue with captured variables at least once, and if you don't know what it is, you can't google for the solution.
Using Tag as in other answers is also a valid solution. Likewise using a data binding template, may be the best solution. Hence I am not voting to close this as a duplicate of one of the 101 questions on captured variables in closures.
EDIT 2:
What you asked for in the comments
<Grid Name="ButtonsContainer">
<ListBox Name="lb_GlobalList">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel>
<Button Tag="{Binding SomeNumberValue}" Content="Click me" Click="Button_Click" />
<TextBlock Text="{Binding Path=SomeNumberValue, StringFormat={}I am from {0}}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
List<SomeBusinessObject> businessObjects = new List<SomeBusinessObject>();
for (int i = 0; i < 5; i++)
{
businessObjects.Add(new SomeBusinessObject() { SomeNumberValue = i, SomeTextValue = "sample text" });
}
lb_GlobalList.ItemsSource = businessObjects;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("I am from " + (sender as Button).Tag.ToString());
}
}
public class SomeBusinessObject
{
public int SomeNumberValue { get; set; }
public string SomeTextValue { get; set; }
}
EDIT:
I am not using your code on purpose so you will understand:
Capture variables in closured
How to use controls without Name property in a proper way (sender object in the event)
How to store any object using the Tag property - even other controls
That what you are doing is wrong on so many levels - DataBindig, DataTemplate etc. as commented in other answer
In my opinion the best solutions is to add some metadata you want to associate with your Button class using Tag and then add Click event the usual way and use
(sender as Button) in the event handler to access the button
see my example
private void Window_Loaded(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Button b = new Button();
b.Tag = i;
b.Click += B_Click;
sp_Container.Children.Add(b);
}
}
private void B_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show((sender as Button).Tag.ToString());
}
ALSO in my opinion what you are doing is wrong. You shoud be needing this example rather in special cases. You have the DataBinding ability in WPF, use it!
Replace
childGridSavebutn[i].Click += (source, e) =>
{
txtBoxOrder[i].Text = "I am from " + i;
MessageBox.Show("I am from " + i); //This message always show "I am From 5"
};
with
childGridSavebutn[i].Tag = i;
childGridSavebutn[i].Click += (source, e) =>
{
var button = source as Button;
var index = (int)button.Tag;
txtBoxOrder[index].Text = "I am from " + index;
MessageBox.Show("I am from " + index);
};
You must need to set the the value of i while subscribing the event, you can use Tag property for this. Otherwise you will get the last updated value of i on every message.
When your current code gets a button click the value of j has completed the loop so for all button clicks you get the last value of j.
But it's easy to fix. Just change your loop like this:
for (int j = 0; j < runTimeIntegerValue; j++)
{
childGridSavebutn[j] = GenerateButton("Save Table");
childSmallgrid[j] = generateRowColumnGrid(1, 1);
Grid.SetColumn(childGridSavebutn[j], 1);
Grid.SetRow(childGridSavebutn[j], i);
childSmallgrid[j].Children.Add(childGridSavebutn[j]);
childGridSavebutn[j].Click += (source, e) =>
{
int j2 = j;
txtBoxOrder[j2].Text = "I am from " + j2;
MessageBox.Show("I am from " + j2);
};
}
The line int j2 = j; captures the value of j at the time the loop is run so that j2 is correct when the click occurs.
I use this solution(in code below) to add multiply buttons on panel. It works ok but it takes too long, when it tries to add a lot of buttons (for an example 40). I want to ask, if any one knows of a better solution for this situation? I was thinking of creating all possible buttons at program start-up, but in this case will start-up take too long, especially if there will be really lot of buttons (this scenario is possible)?
while (data.Read())
{
btnName = Convert.ToString(data["Name"]);
btnColor = (color == string.Empty) ? Convert.ToString(data["Color"]) : color;
categoryId = Convert.ToInt16(data["CategoryId"]);
//both category and article table's contains this data!
if (categoryId == articleCatId || cl == typeof(Category))
{
Button newbtn = new Button();
newbtn.TextAlign = ContentAlignment.MiddleCenter;
newbtn.Click += (sender, e) => method(sender, e);
newbtn.Text = btnName;
newbtn.Name = "button-" + btnName;
newbtn.Height = size;
newbtn.Width = size;
newbtn.Font = new Font("Microsoft Sans Serif", fontH);
newbtn.Location = new Point(paddingL, paddingT);
newbtn.BackColor = ColorTranslator.FromHtml(btnColor);
location.Controls.Add(newbtn);
num += 1;
if ((num - 1) / inline == 1) { paddingT += size; paddingL = 2; num = 1; }
else { paddingL = paddingL + size; }
}
}
You probably cannot reduce the number of buttons you need to create, so you then have some options to speed it up a little bit:
Add the buttons to an object that is not visible. Only when you're done adding buttons, you make the object visible.
Call SuspendLayout on the parent control to stop it from trying to layout itself. Then call ResumeLayout when you're done adding buttons.
Use a more lightweight control than a button, that is more appropriate for the task. For example a Listbox, Combobox, or several checkboxes or option buttons styled as normal buttons.
Write your own lightweight Button control that does exactly what you want but no more.
I have a performance issue with the WPF DataGrid (.net 4.0)
first, some details:
I have a datagrid with an Observable collection as ItemsSource.
this observableCollection itself contains collections of objects, each collection hence being a row, each object being a cell ("logical" cell of course, not actual dataGridCell)
the reason why I do this is because I only know at runtime how many columns I will have in my dataGrid.
then I bind each DataGridCell's value to the value of the object in the "logical" table (= the collection of collections)
now the trouble I have is that I also have to be able to change whatever cell's Properties (like Background, Foreground, FontFamily, etc...) at any time while the app is running.
The solution I came up with is one involving setting the columns' cellStyles with bindings that bind to the "logical" cells' properties
here Is a sample code (no Xaml in my app):
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Width = 1200;
Height = 780;
Top = 60;
Left = 200;
DataGrid dg = new DataGrid();
Content = dg;
ObservableCollection<Row> Source = new ObservableCollection<Row>();
dg.ItemsSource = Source;
dg.SelectionMode = DataGridSelectionMode.Extended;
dg.IsSynchronizedWithCurrentItem = true;
dg.CanUserSortColumns = false;
dg.CanUserReorderColumns = true;
dg.CanUserResizeColumns = true;
dg.CanUserResizeRows = true;
dg.CanUserAddRows = false;
dg.CanUserDeleteRows = false;
dg.AutoGenerateColumns = false;
dg.EnableColumnVirtualization = true;
dg.EnableRowVirtualization = false; // unuseful in my case : I alawys have less lines than the DG can contain
dg.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
dg.GridLinesVisibility = DataGridGridLinesVisibility.None;
dg.HorizontalGridLinesBrush = Brushes.LightGray;
dg.MinRowHeight = 20;
dg.RowHeaderWidth = 20;
for (int i = 0; i < 100; i++)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Binding = new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Text", i));
Style style = new Style(typeof(DataGridCell));
style.Setters.Add(new Setter(DataGridCell.BackgroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Background", i))));
style.Setters.Add(new Setter(DataGridCell.ForegroundProperty, new Binding(String.Format(CultureInfo.InvariantCulture, "[{0}].Foreground", i))));
column.CellStyle = style;
column.Header = "Column " + i;
dg.Columns.Add(column);
}
for (int i = 0; i < 35; i++)
{
Row dgRow = new Row();
Source.Add(dgRow);
for (int j = 0; j < 100; j++)
dgRow.Add(new TextBox() { Text = "cell " + i + "/" + j, Background = Brushes.AliceBlue, Foreground = Brushes.BlueViolet });
}
}
}
public class Row : ObservableCollection<TextBox>
{
}
my problem is: with the VolumnVirtualisation On (I don't need row Virtualization in my case), the grid takes about 2sec to load, and then 1sec each time I move the horizontal scrollbar by a big leap (clic in the scrollBar bg, not the arrow)
this is too much for my purpose
so my question is: am I doing something wrong and if yes, what? what better way to do this do I have?
thanks for reading
If ColumnVirtualization make so problems, why do you need it?
You can do a several improvements, but they can't solve the problem completely.
Change TextBoxes for light-weight objects:
public class TextItem
{
public string Text { get; set; }
public Brush Background { get; set; }
public Brush Foreground { get; set; }
}
public class Row : ObservableCollection<TextItem>
{
}
Enable VirtualizingStackPanel: dg.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, true);
Replace styles with templates:
for (int i = 0; i < 100; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
column.CellTemplate = (DataTemplate)XamlReader.Parse(
"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
"<TextBlock DataContext='{Binding [" + i + "]}' Text='{Binding Text}' Background='{Binding Background}' Foreground='{Binding Foreground}'/>" +
"</DataTemplate>");
column.Header = "Column " + i;
dg.Columns.Add(column);
}
After a lot of time put into this, I came to the conclusion that I've reached the limit.
Here are a few thoughts for those that are dealing with the same issue:
There is no easy way to manage a single cell's visual properties in WPF as of .net 4.0: MS did not plan anything to make this easy so basically you are stuck with 2 possibilities to do this:
get the actual dataGridCell using some kind of helper function and then change its properties directly. This is easily done but can lead to big trouble if virtualization is turned on.
bind each cell's visual properties to dependency properties from your VM inside the dataGridCell's Style. you can use either the DataGrid.CellStyle or the Column.CellStyle to do so, depending on you constraints. This slows the dataGrid quite a bit though, and is quite a hassle to manage.
if like me you have no choice but to use the second option (because I need virtualization), here are a few things to consider:
you are not stuck with C#. There is actually a way to do your CellStyle in Xaml. See Martino's post on this issue. As far as I'm concerned, this works pretty well. I tweaked it a bit so as not to have to use the hack though: I define my style in Xaml and apply it to the Column.CellStyle. Then when I create a column in my code behind, I simply create a new Style inheriting this one, and I add the Tag setter with a binding set to: "[column's Index].Self". This breaks the MVVM model, but I'm not using it anyway and It's easier to maintain like this.
obviously, the more properties you have to bind, the more time it will take for your dataGrid to load, so stick to the minimum (using light-weight objects does make a small difference, as stated by Vorrtex).
while using templates or styles makes absolutely no difference regarding performance, if you are using dataGridTemplateColumns, you'd want to set you bindings up directly in the template instead of adding a style on top of the template, obviously (this does make a small difference with a huge number of data)
if anybody has anything to add to this, please do so! I'm still looking for any idea that can improve things up and would be glad for whatever crazy idea you have on the subject. Even in 3 months...