Create a custom DataGrid's ItemsSource - c#

I am working with DataGrids but I am struggling to binding my data since the number of columns varies depending of the info that has to be showed.
So, what I have tried to do is to create and object which contains all the columns and rows that I need at some point and binding this object to the ItemsSource property. Since I have worked with DataGridViews in WindowsForms I have in mind something like this:
DataTable myTable = new DataTable();
DataColumn col01 = new DataColumn("col 01");
myTable.Columns.Add(col01);
DataColumn col02 = new DataColumn("col 02");
myTable.Columns.Add(col02);
DataRow row = myTable.NewRow();
row[0] = "data01";
row[1] = "data02";
myTable.Rows.Add(row);
row = myTable.NewRow();
row[0] = "data01";
row[1] = "data02";
myTable.Rows.Add(row);
But I haven't been able to find a way to do the same thing in WPF since I need some columns to be DataGridComboBoxColumns for example.
Actually I have read many post about it in this site, but none of them helped to me. I am really lost.
Could anyone help me? I just need to be able to create a table which may contain DataGridTextColumns or `DataGridComboBoxColumns, etc, In order to bind this final object to the DataGrid's ItemsSource property.
Hope someone can help me.

Okay, let me try to take an example which is similar to your needs
Let's assume we use this class:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
And we are willing to display a DataGrid listing the ID, and having as a second column a Button, with the property MyString as content, which, when clicked, launches the ICommand MyCommand which opens in a new window whatever you want.
Here is what you should have on the View side:
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding MyID}" />
<DataGridTemplateColumn Header="Buttons">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding MyString}" Command="{Binding MyCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
This will show a DataGrid taking all the content in an IEnumerable<MyObject> named 'MyList', and shows two columns as defined before.
Now if you need to define the command.
First, I recommend you read this introductory link to MVVM and take the RelayCommand class (that's what we're gonna use for your problem)
So, in your ViewModel, the one which defines the MyList, here is how you should define some of the useful objects:
public ObservableCollection<MyObject> MyList { get; set; }
// blah blah blah
public void InitializeMyList()
{
MyList = new ObservableCollection<MyObject>();
for (int i = 0; i < 5; i++)
{
MyList.Add(InitializeMyObject(i));
}
}
public MyObject InitializeMyObject(int i)
{
MyObject theObject = new MyObject();
theObject.MyID = i;
theObject.MyString = "The object " + i;
theObject.MyCommand = new RelayCommand(param =< this.ShowWindow(i));
return theObject
}
private void ShowWindow(int i)
{
// Just as an exammple, here I just show a MessageBox
MessageBox.Show("You clicked on object " + i + "!!!");
}

A simple example of binding to a ObservableCollection of a custom object. Add more properties to the custom object to match what you want your rows to look like.
using System.Collections.ObjectModel;
public MyClass
{
public ObservableCollection<MyObject> myList { get; set; }
public MyClass()
{
this.DataContext = this;
myList = new ObservableCollection<MyObject>();
myList.Add(new MyObject() { MyProperty = "foo", MyBool = false };
myList.Add(new MyObject() { MyProperty = "bar", MyBool = true };
}
}
public MyObject
{
public string MyProperty { get; set; }
// I believe will result in checkbox in the grid
public bool MyBool { get; set; }
//...as many properties as you want
}
with xaml
<DataGrid ItemsSource= "{Binding myList}" />
Might be some small syntax errors, I wrote that entirely within the SO window.

I am new to WPF and used Damascus's example to learn binding of a List to a datagrid. But when I used his answer I found that my datagrid would populate with the correct number of rows but not with any of the properties from the MyObject class. I did a bit more searching then stumbled across what I had to do by accident.
I had to encapsulate the MyObject class properties to have them show. It wasn't enough to have them be public.
Before:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
After:
public class MyObject
{
private int _myID;
public int MyID
{
get { return _myID; }
set { _myID = value; }
}
private string _myString;
public string MyString
{
get { return _myString; }
set { _myString = value; }
}
private ICommand _myCommand;
public ICommand MyCommand
{
get { return _myCommand; }
set { _myCommand = value; }
}
}
Thank you Damascus for a great example and thank you Dante for a great question. I don't know if this is due to a change in version since your post but hopefully this will help others new to WPF like me.

Related

Binding content list to ComboBox in WPF MVVM

So let me preface by saying that I am very new to WPF and MVVM.
I am using the mvvm design pattern for my application. My goal, is that I need to have two combo boxes loaded with content to select from( in this case, units to convert from and to). The content of these combo boxes is determined by a third combo box which determines the type of units to load.
So for example, the first combo box would let the user select a unit type, such as speed or temperature. So if I select temperature, the other two combo boxes would be loaded with a list of temperature units. Likewise if I select speed, then the list in the other two combo boxes would be replaced with units for speed.
I already have a class that handles the from and to conversion. But I'm a little lost with how to start working with these combo boxes. I have only done some basic things with combo boxes like loading content straight in the xaml. I have seen people make lists and somehow bind them but some it was a little overwhelming.
All I need is a good example and explanation to get me started. Would greatly appreciate it.
Everything you need is a ViewModel class to work with the binding.
Each combo box will binding the ItemSources to a Property in the ViewModel. Everytime the selected of the first combo box is change, you will update the data source of the second combo box.
Here is example of the ViewModel class:
namespace WpfApp1
{
class SampleVM : ViewModelBase
{
private ObservableCollection<UnitEntry> _comboBox1ItemSource;
private ObservableCollection<TypeEntry> _comboBoxTypeItemSource;
private int _selectedTypeIndex;
public ObservableCollection<UnitEntry> ComboBoxUnitItemSource
{
get => _comboBox1ItemSource;
set
{
_comboBox1ItemSource = value;
RaisePropertyChange(nameof(ComboBoxUnitItemSource));
}
}
public ObservableCollection<TypeEntry> ComboBoxTypeItemSource
{
get => _comboBoxTypeItemSource;
set
{
_comboBoxTypeItemSource = value;
RaisePropertyChange(nameof(ComboBoxTypeItemSource));
}
}
public int SelectedTypeIndex
{
get => _selectedTypeIndex;
set
{
_selectedTypeIndex = value;
RaisePropertyChange(nameof(SelectedTypeIndex));
//Here where we will handle the data in the second combo box depend on the Type value when it changed
if(value == 0)
{
ComboBoxUnitItemSource = GetDataUnitType1();
}
else
{
ComboBoxUnitItemSource = GetDataUnitType2();
}
}
}
public SampleVM()
{
InitData();
}
private void InitData()
{
//Init Type data
ComboBoxTypeItemSource = new ObservableCollection<TypeEntry>();
TypeEntry type1 = new TypeEntry(0, "Type 1");
TypeEntry type2 = new TypeEntry(1, "Type 2");
ComboBoxTypeItemSource.Add(type1);
ComboBoxTypeItemSource.Add(type2);
//Selected Index set to default by 0
SelectedTypeIndex = 0;
}
private ObservableCollection<UnitEntry> GetDataUnitType1()
{
//Get your real data instead of fake data below
ObservableCollection<UnitEntry> data = new ObservableCollection<UnitEntry>();
for (int i = 0; i < 5; i++)
{
UnitEntry unitEntry = new UnitEntry(i, $"Type 1 - Entry: {i}");
data.Add(unitEntry);
}
return data;
}
private ObservableCollection<UnitEntry> GetDataUnitType2()
{
//Get your real data instead of fake data below
ObservableCollection<UnitEntry> data = new ObservableCollection<UnitEntry>();
for (int i = 0; i < 5; i++)
{
UnitEntry unitEntry = new UnitEntry(i, $"Type 2 - Entry: {i}");
data.Add(unitEntry);
}
return data;
}
}
public class TypeEntry
{
public int ID { get; set; }
public string Name { get; set; }
public TypeEntry(int id, string name)
{
ID = id;
Name = name;
}
}
public class UnitEntry
{
public int ID { get; set; }
public string Name { get; set; }
public UnitEntry(int id, string name)
{
ID = id;
Name = name;
}
}
}
And here is the xaml class looks like:
<!-- The "Name" value is the Name property in the Entry class-->
<ComboBox Grid.Row="0"
Grid.Column="0"
Width="200"
Height="30"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedIndex="{Binding SelectedTypeIndex}"
ItemsSource="{Binding ComboBoxTypeItemSource}"/>
<ComboBox Grid.Row="0"
Grid.Column="1"
Width="200"
Height="30"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedIndex="0"
ItemsSource="{Binding ComboBoxUnitItemSource}"/>
Finally, important part, you need to assign the ViewModel to the View class:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new SampleVM();
}
}

WPF DataGrid: how to know if ValidationTemplate is enabled

In a WPF application, I have a view with an editable datagrid and a viewmodel. This is an existing codebase, and a problem arises: there are fields in the viewmodel that raises exceptions but those fields are not in the view. A refactor is necessary, but for now we need to implement a visual clue (red border around the row) for the user as a quick fix.
From the viewmodel, I raise an event that a validation took place, and in the code-behind, I want to check in the datagridrow if the ValidationErrorTemplate is enabled.
As the elements added by the ValidationErrorTemplate are added as AdornerLayer outside of the datagridrows, it seems that I have no clue to which datagridrow this is coupled?
I have not much code to show, just that I get to the correct datagridrow for which viewmodel a validation took place:
private void OnValidationEvent(ValidationEventArgs e)
{
var rows = BoekingDatagrid.GetDataGridRow(e.ID);
if (e.HasErrors)
{
if (errorBorder == null)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
var vm = row.DataContext as ItemBaseViewModel;
LogValidationErrors(vm, UserContext);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
Every column has the following xaml, with a Validation.ErrorTemplate:
<DataGridTemplateColumn Header="Name"
CanUserResize="False"
SortMemberPath ="Name"
Width="130"
MinWidth="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ViewMode, Converter={StaticResource ViewModeToBoolean}, ConverterParameter=name}"
Validation.ErrorTemplate="{StaticResource ResourceKey=ErrorTemplate2_Grid}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ErrorTemplate2_Grid adds a red border and tooltip to the cell.
In Visual Live Tree, you can see that the rows and the error visuals, but they are not nested:
The question is: how can I find out if there are visual error elements added to the datagridrow, when the viewmodel is invalid?
Not sure what BindingGroup exactly does, but the databound rows does have there own BindingGroups with only the databindings of the controls in the row. And a very convenient property: HasValidationError
private void OnValidationEvent(ValidationEventArgs e)
{
var row = BoekingDatagrid.GetDataGridRow();
if (row != null)
{
if (e.HasErrors)
{
if (!row.BindingGroup.HasValidationError)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
}
else
{
row.BorderThickness = new Thickness(0);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
}
Answered for mine and others future reference.
Validation in WPF is typically done using the interface INotifyDataErrorInfo. There are other ways too, but the ViewModel raising an event that the View handles isn't typically one of them. A very simplified model class could look like this:
public class Model : INotifyDataErrorInfo
{
public Model(int myInt, string myString)
{
MyInt = myInt;
MyString = myString;
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get
{
try
{
return MyInt == 88 || MyString == "foo";
}
catch (Exception)
{
return true;
}
}
}
public int MyInt
{
get { throw new NotImplementedException(); }
set { }
}
public string MyString { get; set; }
public IEnumerable GetErrors(string propertyName)
{
try
{
if (propertyName == nameof(MyInt) && MyInt == 88)
return new string[] { "MyInt must not be 88" };
if (propertyName == nameof(MyString) && MyString == "foo")
return new string[] { "MyString must not be 'foo'" };
return new string[0];
}
catch (Exception)
{
return new string[] { "Exception" };
}
}
}
A quick and diry window could look like this:
public partial class MainWindow : Window
{
public MainWindow()
{
Models = new List<Model>
{
new Model(1,"hello"),
new Model(1,"foo"),
new Model(88,"hello"),
new Model(2,"world"),
};
DataContext = this;
InitializeComponent();
}
public List<Model> Models { get; set; }
}
whereas the XAML just contains <DataGrid ItemsSource="{Binding Models}" />
A red rectangle in case of an error is default. Just apply your custom template and do not set any ValidationErrorTemplate.

Auto Generate columns with ObservableCollection<object>

I'm trying to bind to a datagrid a collection of object where as this property will change depending on the requirement/s.
In my VM, I have this property:
private ObservableCollection<object> _productData;
public ObservableCollection<object> ProductData
{
get { return _productData; }
set { _productData = value; }
}
And in my View:
<DataGrid CanUserAddRows="False"
ItemsSource="{Binding ProductData, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="True"
AutoGenerateColumns="True"
Margin="0 2 0 0" />
Is it possible to auto generate columns based on the "object" supplied?
Ex:
ProductData = new ObservableCollection<object>(SomethingThatReturnsClassAList());
The way DataGrid Auto Generates Columns is pretty robust, but unfortunately it does not work the way you want it to. One way or another, you need to tell it what columns to expect. If you give it an object type, it isn't going to reflect over what type is assigned to the 'object', it's just going to reflect over 'System.object', which will net you 0 columns auto generated. I'm not smart enough to explain this whole debacle. I just want to jump into the solution I came up with, which should work well for your purposes.
I actually surprised myself by making a working example. There's a few things going on here that need some 'splaining.
I left your XAML alone, it's still
<DataGrid CanUserAddRows="False"
ItemsSource="{Binding ProductData, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="True"
AutoGenerateColumns="True"
Margin="0 2 0 0" />
However, in the ViewModel, I've made a decision that hopefully you can work with, to changed ProducData from an ObservableCollection<> to a DataTable. The binding will still work the same, and the UI will update appropriately.
Your original intent was to make the ObservableCollection of type object to make things more generic, but here I've implemented the Factory Pattern to show a way to create multiple types without making things too complicated. You can take it or leave it for what it's worth.
ViewModel (I'm using Prism boilerplate, replace BindableBase with your implementation of INotifyPropertyChanged
public class ViewAViewModel : BindableBase
{
private DataTable _productData;
private IDataFactory _dataFactory;
public ViewAViewModel(IDataFactory dataFactory)
{
_dataFactory = dataFactory;
}
public DataTable ProductData
{
get { return _productData; }
set { _productData = value; OnPropertyChanged(); }
}
public void Load()
{
ProductData = _dataFactory.Create(typeof(FooData));
}
}
DataFactory
public interface IDataFactory
{
DataTable Create(Type t);
}
public class DataFactory : IDataFactory
{
public DataTable Create(Type t)
{
if (t == typeof(FooData))
{
return new List<FooData>()
{
new FooData() {Id = 0, AlbumName = "Greatest Hits", IsPlatinum = true},
new FooData() {Id = 1, AlbumName = "Worst Hits", IsPlatinum = false}
}.ToDataTable();
}
if (t == typeof(BarData))
{
return new List<BarData>()
{
new BarData() {Id = 1, PenPointSize = 0.7m, InkColor = "Blue"},
new BarData() {Id = 2, PenPointSize = 0.5m, InkColor = "Red"}
}.ToDataTable();
}
return new List<dynamic>().ToDataTable();
}
}
Base class and Data Models
public abstract class ProductData
{
public int Id { get; set; }
}
public class FooData : ProductData
{
public string AlbumName { get; set; }
public bool IsPlatinum { get; set; }
}
public class BarData : ProductData
{
public decimal PenPointSize { get; set; }
public string InkColor { get; set; }
}
So for usage, you can swap FooData with BarData, or any type derived of ProductData
public void LoadFooData()
{
ProductData = _dataFactory.Create(typeof(FooData));
}
Last but not least, I found this little gem somewhere on SO (If I find it again, will credit the author) This is an extension method to generate the DataTable
from an IList<T> with column names based on the properties of T.
public static class DataFactoryExtensions
{
public static DataTable ToDataTable<T>(this IList<T> data)
{
PropertyDescriptorCollection properties =
TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
foreach (PropertyDescriptor prop in properties)
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
foreach (T item in data)
{
DataRow row = table.NewRow();
foreach (PropertyDescriptor prop in properties)
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
table.Rows.Add(row);
}
return table;
}
}
public void LoadBarData()
{
ProductData = _dataFactory.Create(typeof(BarData));
}
And in either case, INPC will update your UI.
Update based on your addition to the post
To implement this using a method that returns a list like your example; SomethingThatReturnsClassAList()
Just update factory like so. (Notice how easy it is to change your code to meet new requirements when you use the Factory Pattern? =)
public class DataFactory : IDataFactory
{
public DataTable Create(Type t)
{
if (t == typeof(FooData))
{
return SomethingThatReturnsClassAList().ToDataTable();
}
if (t == typeof(BarData))
{
return SomethingThatReturnsClassBList().ToDataTable();
}
return new List<dynamic>().ToDataTable();
}
}

How to set SelectedValue of a Combo binded to a Dictionary

I wanna bind a dictionary to a combobox.
My code is this (here I simplified it a little)
public class MyObject
{
private AnotherObject _obj;
public MyObject() { }
public MyObject(AnotherObject obj)
{
_obj = obj;
}
public string ObjType
{
get { return (_obj != null ? _obj.GetType() : "NO OBJECT" ); }
}
public string ObjString
{
get { return (_obj != null ? _obj.ToString() : "NO OBJECT" ); }
}
}
public class MyClass
{
private Dictionary<string, MyObject> _BindingButtons;
public MyClass()
{
_BindingButtons["KEY_1"] = new MyObject( CreateAnotherObject() );
_BindingButtons["KEY_2"] = new MyObject( CreateAnotherObject() );
...
...
}
public Dictionary<string, MyObject> BindingButtons
{
get { return _BindingButtons; }
}
}
Now, in the xaml I wrote
<ComboBox ItemsSource="{Binding BindingButtons}"
DisplayMemberPath="ObjString"
SelectedValuePath="KEY_1"
SelectedValue="{Binding ???? }"/>
and I have two problem
the combo list is empty (the dictionary is not empty)
Is there a way to bind the SelectedValue to something like:
"{Binding BindingButtons[KEY_1]}"
without create a property for each combo?
Where I wrong?
Regards.
[EDIT]
regarding my question on SelectedValue...
Suppose to have 10 combos with the same ItemsSource. Now, instead of write 10 properties (one for each combo), is there a way to write something like that (to handle SelectedItem):
class Test
{
private string[] _Items = new string[10];
public string GetItem(int index)
{
return _Items[index];
}
public void SetItem(int index, string value)
{
_Items[index] = value;
}
}
Dictionary has a collection property Values which you can leverage here
according to your data model this should work
<ComboBox ItemsSource="{Binding BindingButtons.Values}"
DisplayMemberPath="ObjString"
SelectedItem="{Binding SelectedItemInVM}"/>
I was not sure about the purpose of SelectedValue in your question, I replaced it with SelectedItem in example, you may adjust according to your needs.
EDIT
answer for your edit
you have two options to bind the individual combobox to an array
but before that you need to convert the array field to a public property and initialize accordingly, otherwise binding may fail
public string[] Items { get; set; }
and then bind using indexers Items[0] etc.
<ComboBox ItemsSource="{Binding BindingButtons.Values}"
DisplayMemberPath="ObjString"
SelectedValuePath="ObjType"
SelectedValue="{Binding Items[0]}"/>
or create the array of MyObject type
public MyObject[] Items { get; set; }
and binding remain same but using SelectedItem property instead of SelectedValue
<ComboBox ItemsSource="{Binding BindingButtons.Values}"
DisplayMemberPath="ObjString"
SelectedItem="{Binding Items[0]}"/>
make sure to initialize your array in constructor Items = new string[10]; or Items = new MyObject[10]; 10 is number of combo you have

DataGrid not updating right away

In my program I have a DataGrid implemented through MVVM. Next to this DataGrid is a button that executes a command that I've named, "Fill Down". It takes one of the columns and copies a string to every cell in that column. The problem is that the view doesn't make the change until I change the page and then go back to the page with the DataGrid. Why is this happening, and what can I do to fix it?
xaml:
<Button Command="{Binding FillDown}" ... />
<DataGrid ItemsSource="{Binding DataModel.Collection}" ... />
ViewModel:
private Command _fillDown;
public ViewModel()
{
_fillDown = new Command(fillDown_Operations);
}
//Command Fill Down
public Command FillDown { get { return _fillDown; } }
private void fillDown_Operations()
{
for (int i = 0; i < DataModel.NumOfCells; i++)
{
DataModel.Collection.ElementAt(i).cell = "string";
}
//**I figured that Notifying Property Change would solve my problem...
NotifyPropertyChange(() => DataModel.Collection);
}
-Please let me know if there is anymore code you would like to see.
Yes, sorry my Collection is an ObservableCollection
Call NotifyPropertyChanged() in the setter of your properties:
public class DataItem
{
private string _cell;
public string cell //Why is your property named like this, anyway?
{
get { return _cell; }
set
{
_cell = value;
NotifyPropertyChange("cell");
//OR
NotifyPropertyChange(() => cell); //if you're using strongly typed NotifyPropertyChanged.
}
}
}
Side Comment:
change this:
for (int i = 0; i < DataModel.NumOfCells; i++)
{
DataModel.Collection.ElementAt(i).cell = "string";
}
to this:
foreach (var item in DataModel.Collection)
item.cell = "string";
which is much cleaner and readable.

Categories