I am making WPFToolkit based Graph where i am trying to update AreaSeries every time i click on a button.
I have implemented INotifyPropertyChanged on my data class. but when i reload the data in the source object id doesn't updates in chart(target object)
the code is as below:
public partial class MainWindow : Window
{
static List<Ready4LOS> Ready4LOS = new List<Data.Ready4LOS>();
public MainWindow()
{
InitializeComponent();
chart1.DataContext = Ready4LOS;
InitChart();
LoadData();
}
private void LoadData()
{
var path = #"zxzxzxz.log";
Ready4LOS.Clear();
List<APISTATDataModel> daa = APISTATDataModel.GetFromFile(path, new string[] { "|" }, "Ready4TOS");
List<APISTATDataModel> lastn = daa.GetRange(daa.Count - 10, 10);
foreach (APISTATDataModel d in lastn)
{
Ready4LOS.Add(new Ready4LOS() { Case = d.Current_Count, Time = d.Current_Time });
}
}
private void InitChart()
{
System.Windows.Data.Binding indi = new System.Windows.Data.Binding("Case");
System.Windows.Data.Binding dep = new System.Windows.Data.Binding("Time");
dep.Mode = System.Windows.Data.BindingMode.OneWay;
indi.Mode = System.Windows.Data.BindingMode.OneWay;
AreaSeries ares = new AreaSeries();
ares.ItemsSource = Ready4LOS;
ares.IndependentValueBinding = dep;
ares.DependentValueBinding = indi;
ares.Title = "Ready4LOS";
DateTimeAxis dta = new DateTimeAxis();
dta.Interval = 10;
dta.IntervalType = DateTimeIntervalType.Minutes;
dta.Title = "Time";
dta.Orientation = AxisOrientation.X;
// dta.Minimum = DateTime.Now.AddMinutes(-90);
// dta.Maximum = DateTime.Now;
LinearAxis yaxis = new LinearAxis();
yaxis.Minimum = 0;
yaxis.Interval = 2;
yaxis.Title = "Case";
yaxis.Orientation = AxisOrientation.Y;
yaxis.ShowGridLines = true;
chart1.Axes.Add(yaxis);
chart1.Axes.Add(dta);
chart1.Series.Add(ares);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
LoadData();
chart1.UpdateLayout();
}
}
}
the data model is here
class Ready4LOS : INotifyPropertyChanged
{
int _case;
DateTime _time;
public int Case
{
get
{
return _case;
}
set
{
_case = value;
NotifyPropertyChanged("Case");
}
}
public DateTime Time
{
get
{
return _time;
}
set
{
_time = value;
NotifyPropertyChanged("Time");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
It loads perfectly when it starts as i've called the LoadData() in the beginning.
The problem is when i click on the refresh button it loads the data in the source object but the target object's data in not updated i.e. chart is not updated it remains the same as of initial data.
Use ObservableCollection<Ready4LOS>, not List<Ready4LOS>. ObservableCollection<> already implements INotifyPropertyChanged and also INotifyCollectionChanged. Your implementation of INotifyPropertyChanged for Ready4LOS may only be necessary if you're going to dynamically change values for Case and Time for existing Ready4LOS already in your collection.
When I call Generate() the events associated with the ObservableCollection (X) is not fired.
What am I doing wrong?
The code:
MyControl.xaml.cs
public ObservableCollection<double> X
{
get { return (ObservableCollection<double>)GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public static readonly DependencyProperty XProperty =
DependencyProperty.Register(
"X", typeof(ObservableCollection<double>),
typeof(MyControl),
new PropertyMetadata(
new ObservableCollection<double>(),
new PropertyChangedCallback(OnXChanged)));
private static void OnXChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as MyControl;
// DoSomething
}
A XAML random page that uses MyControl:
<local:MyControl
Title="Test"
X="{Binding TestX, Mode=TwoWay}"
/>
That page's .cs
public sealed partial class MainPage : Page
{
public ObservableCollection<double> TestX { get; set; }
private static Random rand_ = new Random();
public MainPage()
{
this.InitializeComponent();
TestX = new ObservableCollection<double>();
}
private void Generate()
{
TestX.Clear();
for (int i = 0; i < 100; ++i)
{
TestX.Add(rand_.Next(1, 100));
}
}
....
}
Please note that I don't see any BindingExpression error in the output window.
Update
I noticed that if do like this in the page, it works:
TestX = new ObservableCollection<double>();
this.MyUserControlInstance.X = TestX;
You're missing two things there:
First:
Make sure you set the DataContext in your constructor:
public MainPage()
{
this.InitializeComponent();
DataContext = this; // Important, but you should use a seperated ViewModel instead
TestX = new ObservableCollection<double>();
}
Second:
Your class is missing the INotifyPropertyChanged implementation, as well as the PropertyChanged call for your TestX property:
private ObservableCollection<double> _testX;
public ObservableCollection<double> TestX
{
get { return _testX; }
set
{
if (value == _testX) return;
_testX = value;
OnPropertyChanged();
}
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
Side note: Do MVVM!
Please assume this entire question deals in code, without any XAML.
I have a static ObservableCollection named myStaticList. It's a part of a non-static class named myClass.
public class myClass
{
public static ObservableCollection<CheckBoxStructure> myStaticList { get; set; }
static myClass()
{
myStaticList = new ObservableCollection<CheckBoxStructure>();
}
}
And the definition of CheckBoxStructure:
public class CheckBoxStructure
{
public string Description { get; set; }
public bool IsSelected { get; set; }
}
In addition, there's an array of checkboxes called checkBoxArray[], holding 3 elements. each checkbox has as content a textbox.
What I want to do is programmatically bind (two-way) these two, in such a manner that the IsChecked property of the checkboxes in the checkBoxArray[] array will bind to the IsSelected property of the myStaticList's CheckBoxStructure, and similarly so between the text of the textboxes inthe checkboxes' content and the Description property of the myStaticList's CheckBoxStructure.
In addition, I would like to avoid using loops, since it is preferable that this two lists will update each other if they change in size.
How is this possible?
Thanks!
Using XAML, an easy way would be to the declare an ItemsControl and a DataTemplate for it so that you can have a UserControl (CheckBox and TextBox inside) with its DataContext being a CheckBoxStructure. This way the bindings work between CheckBox.IsChecked and IsSelected property and between TextBox.Text and Description property.
If you need to this only in code then you would have to create same behavior (ItemsControl with a DataTemplate). You have at least 2 options
1.
DataTemplate template = new DataTemplate();
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(StackPanel));
template.VisualTree = factory;
FrameworkElementFactory childFactory = new FrameworkElementFactory(typeof(CheckBox));
childFactory.SetBinding(CheckBox.IsChecked, new Binding("IsSelected"));
factory.AppendChild(childFactory);
childFactory = new FrameworkElementFactory(typeof(TextBox));
childFactory.SetBinding(Label.ContentProperty, new Binding("Description"));
factory.AppendChild(childFactory);
2.
MemoryStream sr = null;
ParserContext pc = null;
string xaml = string.Empty;
xaml = "<DataTemplate><StackPanel><TextBlock Text="{Binding Description"/><CheckBox IsChecked="{Binding IsSelected"/></StackPanel></DataTemplate>";
sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));
pc = new ParserContext();
pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
DataTemplate datatemplate = (DataTemplate)XamlReader.Load(sr, pc);
this.Resources.Add("dt", datatemplate);
Later edit, after discussion from comments; this example works only one way of binding but is easily to make it two ways. Please note that this is only a trivial example of a concept and is not complete: you need to modify the list classes to suit how you wish for objects to be paired, you may need to add more guards for corner cases, you may need to make it thread safe and so on...
First the basic binding objects:
class Binder
{
public Binder()
{
_bindings = new Dictionary<string, List<string>>();
}
private INotifyPropertyChanged _dataContext;
public INotifyPropertyChanged DataContext
{
get { return _dataContext; }
set
{
if (_dataContext != null)
{
_dataContext.PropertyChanged -= _dataContext_PropertyChanged;
}
_dataContext = value;
_dataContext.PropertyChanged += _dataContext_PropertyChanged;
}
}
void _dataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_bindings.ContainsKey(e.PropertyName))
{
var bindableType = _dataContext.GetType();
var bindableProp = bindableType.GetProperty(e.PropertyName);
if (bindableProp == null)
{
return;
}
var binderType = this.GetType();
foreach (var binderPropName in _bindings[e.PropertyName])
{
var binderProp = binderType.GetProperty(binderPropName);
if (binderProp == null)
{
continue;
}
var value = bindableProp.GetValue(_dataContext);
binderProp.SetValue(this, value);
}
}
}
Dictionary<string, List<string>> _bindings;
public void AddBinding(string binderPropertyName, string bindablePropertyName)
{
if (!_bindings.ContainsKey(bindablePropertyName))
{
_bindings.Add(bindablePropertyName, new List<string>());
}
_bindings[bindablePropertyName].Add(bindablePropertyName);
}
}
class Bindable : INotifyPropertyChanged
{
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Then the holding lists for them:
class BindableList<T> : List<T> where T : Bindable
{
public event Action<T> ItemAdded;
public new void Add(T item)
{
base.Add(item);
NotifyItemAdded(item);
}
private void NotifyItemAdded(T item)
{
if (ItemAdded != null)
{
ItemAdded(item);
}
}
}
class BinderList<T> : List<T> where T : Binder
{
public BinderList()
{
_bindingRules = new Dictionary<string, string>();
}
private BindableList<Bindable> _dataContextList;
public BindableList<Bindable> DataContextList
{
get { return _dataContextList; }
set
{
if (_dataContextList != null)
{
_dataContextList.ItemAdded -= _dataContextList_ItemAdded;
}
_dataContextList = value;
_dataContextList.ItemAdded += _dataContextList_ItemAdded;
}
}
void _dataContextList_ItemAdded(Bindable obj)
{
foreach (var pair in _bindingRules)
{
this[Count-1].AddBinding(pair.Key, pair.Value);
this[Count - 1].DataContext = obj;
}
}
private Dictionary<string, string> _bindingRules;
public void AddBindingRule(string binderPropertyName, string bindablePropertyName)
{
_bindingRules.Add(binderPropertyName, bindablePropertyName);
}
}
Now the actual classes with properties:
class BinderElement : Binder
{
private string _description;
public string Description
{
get { return _description; }
set { _description = value; }
}
}
class BindableElement : Bindable
{
private string _description;
public string Description
{
get
{
return _description;
}
set
{
_description = value;
NotifyPropertyChanged("Description");
}
}
}
And an example to use them:
static void Main(string[] args)
{
var bindableList = new BindableList<Bindable>();
var binderList = new BinderList<BinderElement>()
{
new BinderElement(),
new BinderElement()
};
binderList.DataContextList = bindableList;
binderList.AddBindingRule("Description", "Description");
bindableList.Add(new BindableElement());
bindableList.Add(new BindableElement());
((BindableElement)bindableList[1]).Description = "This should arrive in BinderElement Description property";
Console.WriteLine(binderList[1].Description);
Console.ReadLine();
}
I am quite new to WPF (from Winforms). I am using .Net 4.5 and the default DataGrid that comes along with the framework in WPF. The columns are created dynamically because I do not know at compile time. Now, based on data some columns will be read-only and some will be of ComboBox type.
How can I apply this logic dynamically while creating the columns dynamically as shown below. here is the code which I wrote so far. Whenever the data changes, the columns are generated dynamically based on the data.
Also, how do I generate "different types" of column dynamically (ComboBox, TextBox, etc...) based on data. The MVVM-ish way in WPF is kind of restricting me because I do not have much knowledge about templating. I am sure it should be easy once I get through.
NB: Currently all this is working fine. I have a read-only databound grid. But, there is no support for selective editable columns and selective ComboBox columns.
public class DatagridExtension {
public static readonly DependencyProperty RefDataSourceProperty =
DependencyProperty.RegisterAttached(
"RefDataSource",
typeof(RefDataRecord),
typeof(DatagridExtension),
new PropertyMetadata( default(RefDataRecord), OnRefDataSourceChanged)
);
private static void OnRefDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as DataGrid;
var dataSource = e.NewValue as RefDataRecord;
grid.ItemsSource = dataSource;
grid.Columns.Clear();
int count = 0;
foreach (var col in dataSource.Columns)
{
grid.Columns.Add(
new DataGridTextColumn
{
Header = col.Name,
Binding = new Binding(string.Format("[{0}]", count))
}
);
count++;
}
}
public static RefDataRecord GetRefDataSource(DependencyObject dependencyObject)
{
return (RefDataRecord) dependencyObject.GetValue(RefDataSourceProperty);
}
public static void SetRefDataSource(DependencyObject dependencyObject, RefDataRecord value)
{
dependencyObject.SetValue(RefDataSourceProperty, value);
}
}
http://msdn.microsoft.com/en-us/library/system.windows.controls.datagridtemplatecolumn.celltemplate(v=vs.95).aspx
WPF DataGrid creates DataGridComboBoxColumn by default if data source property type derives from Enum and sets DataGridColumn.IsReadyOnly by default if property doesn't have public setter or if property has ReadOnlyAttribute with ReadOnlyAttribute.IsReadOnly = true.
I will now show how to customize DataGrid column generation if your data source properties do not satisfy default conditions stated above.
Firstly, I will introduce two attributes used to specify that property is read-only (EditableAttribute) and that property should be visualized as ComboBox with predefined drop-down items (NameValueAttribute).
Here is EditableAttribute.cs:
using System;
namespace WpfApplication
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class EditableAttribute : Attribute
{
public bool AllowEdit { get; set; }
}
}
Here is NameValueAttribute.cs:
using System;
namespace WpfApplication
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class NameValueAttribute : Attribute
{
public string Name { get; set; }
public object Value { get; set; }
}
}
Next, we need some sample classes that will be used for demonstration.
So here is Person.cs class that will represent a single item (row) in a DataGrid:
using System.ComponentModel;
namespace WpfApplication
{
public class Person : ObservableObject
{
private string name;
private string surname;
private char gender;
public string Name
{
get { return this.name; }
set { this.SetValue(ref this.name, value, "Name"); }
}
[Editable(AllowEdit = false)]
public string Surname
{
get { return this.surname; }
set { this.SetValue(ref this.surname, value, "Surname"); }
}
[NameValue(Name = "Male", Value = 'M')]
[NameValue(Name = "Female", Value = 'F')]
public char Gender
{
get { return this.gender; }
set { this.SetValue(ref this.gender, value, "Gender"); }
}
}
}
Notice how Surname property has EditableAttribute applied and Gender property has NameValueAttributes applied.
And here is People.cs class that will represent DataGrid's data source:
using System.Collections.ObjectModel;
namespace WpfApplication
{
public class People : ObservableCollection<Person>
{
public People()
{
for (int i = 0; i < 100; ++i)
this.Items.Add(new Person()
{
Name = "Name " + i,
Surname = "Surname " + i,
Gender = i % 2 == 0 ? 'M' : 'F'
});
}
}
}
Base class for Person is ObservableObject.cs which is common to all data-binding applications:
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication
{
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
protected void SetValue<T>(ref T field, T value, string propertyName)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
}
}
Now, here is a XAML for MainWindow.xaml that hosts DataGrid control:
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<Window.Resources>
<local:People x:Key="itemsSource"/>
</Window.Resources>
<DataGrid ItemsSource="{StaticResource itemsSource}" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
Crucial part is DataGrid.AutoGeneratingColumn event handler OnAutoGeneratingColumn.
This event gets fired after DataGrid generates a DataGridColumn and is fired once for every auto-generated column. It is used to customize the auto-generated column or specify different one, depending on the provided data source property.
Here is MainWindow.xaml.cs code-behind in which OnAutoGeneratingColumn event handler does exactly that. It customized generated column by setting it as read-only if data source property has EditableAttribute with AllowEdit = false, and it overrides auto-generated column with DataGridComboBoxColumn if data source property has NameValueAttributes:
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var propertyDescriptor = (PropertyDescriptor)e.PropertyDescriptor;
var dataBoundColumn = (DataGridBoundColumn)e.Column;
var comboBoxColumn = GenerateComboBoxColumn(propertyDescriptor, dataBoundColumn);
if (comboBoxColumn != null)
e.Column = comboBoxColumn;
if (IsReadOnlyProperty(propertyDescriptor))
e.Column.IsReadOnly = true;
}
private static DataGridComboBoxColumn GenerateComboBoxColumn(PropertyDescriptor propertyDescriptor, DataGridBoundColumn dataBoundColumn)
{
var nameValueAttributes = Attribute.GetCustomAttributes(propertyDescriptor.ComponentType.GetProperty(propertyDescriptor.Name)).OfType<NameValueAttribute>().ToArray();
if (nameValueAttributes.Length > 0)
return new DataGridComboBoxColumn()
{
ItemsSource = nameValueAttributes,
DisplayMemberPath = "Name",
SelectedValuePath = "Value",
SelectedValueBinding = dataBoundColumn.Binding
};
else
return null;
}
private static bool IsReadOnlyProperty(PropertyDescriptor propertyDescriptor)
{
var editableAttribute = propertyDescriptor.Attributes.OfType<EditableAttribute>().FirstOrDefault();
return editableAttribute != null ? !editableAttribute.AllowEdit : false;
}
}
}
UPDATE FOR DYNAMIC CASE:
WPF supports dynamic reflection with ICustomTypeDescriptor implemented on data items and ITypedList implemented on collection.
Also, .NET 4.5 supports ICustomTypeProvider, but since I do not have .NET 4.5 installed, I haven't tested it.
NameValueAttribute.cs is same as before.
Here is very simple implementation of ICustomTypeDescriptor and ITypedList in a working sample:
DataProperty.cs
using System;
using System.ComponentModel;
namespace WpfApplication
{
public class DataProperty : PropertyDescriptor
{
private readonly Type propertyType;
private readonly bool isReadOnly;
private readonly Attribute[] attributes;
public DataProperty(string propertyName, Type propertyType, bool isReadOnly, params Attribute[] attributes)
: base(propertyName, null)
{
this.propertyType = propertyType;
this.isReadOnly = isReadOnly;
this.attributes = attributes;
}
protected override Attribute[] AttributeArray
{
get { return this.attributes; }
set { throw new NotImplementedException(); }
}
public override Type ComponentType
{
get { return typeof(DataRecord); }
}
public override Type PropertyType
{
get { return this.propertyType; }
}
public override bool IsReadOnly
{
get { return this.isReadOnly; }
}
public override object GetValue(object component)
{
return ((DataRecord)component)[this.Name];
}
public override void SetValue(object component, object value)
{
if (!this.isReadOnly)
((DataRecord)component)[this.Name] = value;
}
#region Not implemented PropertyDescriptor Members
public override bool CanResetValue(object component)
{
throw new NotImplementedException();
}
public override void ResetValue(object component)
{
throw new NotImplementedException();
}
public override bool ShouldSerializeValue(object component)
{
throw new NotImplementedException();
}
#endregion
}
}
DataRecord.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication
{
public class DataRecord : INotifyPropertyChanged, ICustomTypeDescriptor
{
public event PropertyChangedEventHandler PropertyChanged;
internal ITypedList container;
private readonly IDictionary<string, object> values = new SortedList<string, object>();
public object this[string propertyName]
{
get
{
object value;
this.values.TryGetValue(propertyName, out value);
return value;
}
set
{
if (!object.Equals(this[propertyName], value))
{
this.values[propertyName] = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
{
return this.container.GetItemProperties(null);
}
#region Not implemented ICustomTypeDescriptor Members
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetClassName()
{
throw new NotImplementedException();
}
string ICustomTypeDescriptor.GetComponentName()
{
throw new NotImplementedException();
}
TypeConverter ICustomTypeDescriptor.GetConverter()
{
throw new NotImplementedException();
}
EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
{
throw new NotImplementedException();
}
PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
{
throw new NotImplementedException();
}
EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
{
throw new NotImplementedException();
}
PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
{
throw new NotImplementedException();
}
object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
{
throw new NotImplementedException();
}
#endregion
}
}
DataRecordCollection.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace WpfApplication
{
public class DataRecordCollection<T> : ObservableCollection<T>, ITypedList where T : DataRecord
{
private readonly PropertyDescriptorCollection properties;
public DataRecordCollection(params DataProperty[] properties)
{
this.properties = new PropertyDescriptorCollection(properties);
}
protected override void InsertItem(int index, T item)
{
item.container = this;
base.InsertItem(index, item);
}
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
{
return this.properties;
}
string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
{
throw new NotImplementedException();
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
MainWindow.xaml.cs:
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var records = new DataRecordCollection<DataRecord>(
new DataProperty("Name", typeof(string), false),
new DataProperty("Surname", typeof(string), true),
new DataProperty("Gender", typeof(char), false, new NameValueAttribute() { Name = "Male", Value = 'M' }, new NameValueAttribute() { Name = "Female", Value = 'F' }));
for (int i = 0; i < 100; ++i)
{
var record = new DataRecord();
record["Name"] = "Name " + i;
record["Surname"] = "Surname " + i;
record["Gender"] = i % 2 == 0 ? 'M' : 'F';
records.Add(record);
}
this.dataGrid.ItemsSource = records;
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Column.Header = ((PropertyDescriptor)e.PropertyDescriptor).DisplayName;
var propertyDescriptor = (PropertyDescriptor)e.PropertyDescriptor;
var dataBoundColumn = (DataGridBoundColumn)e.Column;
var comboBoxColumn = GenerateComboBoxColumn(propertyDescriptor, dataBoundColumn);
if (comboBoxColumn != null)
e.Column = comboBoxColumn;
}
private static DataGridComboBoxColumn GenerateComboBoxColumn(PropertyDescriptor propertyDescriptor, DataGridBoundColumn dataBoundColumn)
{
var nameValueAttributes = propertyDescriptor.Attributes.OfType<NameValueAttribute>().ToArray();
if (nameValueAttributes.Length > 0)
return new DataGridComboBoxColumn()
{
ItemsSource = nameValueAttributes,
DisplayMemberPath = "Name",
SelectedValuePath = "Value",
SelectedValueBinding = dataBoundColumn.Binding
};
else
return null;
}
}
}
Firstly, one of the main advantages of WPF to WinForms is ability to declare user interface using templates. And you should avoid declaring UI components in code as as possible.
As i understand you want to display collection of different objects based on object type/data.
The best way to implement such logic - implement your own TemplateSelector
I suggest you read next articles:
http://www.wpftutorial.net/DataGrid.html
http://www.switchonthecode.com/tutorials/wpf-tutorial-how-to-use-a-datatemplateselector
P.S.
For reference. Example of declaring DataTemplate in code:
//create the data template
DataTemplate cardLayout = new DataTemplate();
cardLayout.DataType = typeof(CreditCardPayment);
//set up the stack panel
FrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(StackPanel));
spFactory.Name = "myComboFactory";
spFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
//set up the card holder textblock
FrameworkElementFactory cardHolder = new FrameworkElementFactory(typeof(TextBlock));
cardHolder.SetBinding(TextBlock.TextProperty, new Binding("BillToName"));
cardHolder.SetValue(TextBlock.ToolTipProperty, "Card Holder Name");
spFactory.AppendChild(cardHolder);
//set up the card number textblock
FrameworkElementFactory cardNumber = new FrameworkElementFactory(typeof(TextBlock));
cardNumber.SetBinding(TextBlock.TextProperty, new Binding("SafeNumber"));
cardNumber.SetValue(TextBlock.ToolTipProperty, "Credit Card Number");
spFactory.AppendChild(cardNumber);
//set up the notes textblock
FrameworkElementFactory notes = new FrameworkElementFactory(typeof(TextBlock));
notes.SetBinding(TextBlock.TextProperty, new Binding("Notes"));
notes.SetValue(TextBlock.ToolTipProperty, "Notes");
spFactory.AppendChild(notes);
//set the visual tree of the data template
cardLayout.VisualTree = spFactory;
//set the item template to be our shiny new data template
drpCreditCardNumberWpf.ItemTemplate = cardLayout;
but as i say above, you should avoid this.
This is the correct answer - http://www.paulstovell.com/dynamic-datagrid (see the template creation logic dynamically. Its clever).
And, MMVM will be achieved like this - http://www.codeproject.com/Articles/36462/Binding-a-ListView-to-a-Data-Matrix (almost what I have posted in the question)
I was away from the Internet for a few days, but I think that I have found the better approach with simplified PropertyDescriptor architecture which doesn't require to implement ICustomTypeDescriptor. Here is the entire code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfApplication
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var records = new RecordCollection(new Property("Name"), new Property("Surname"));
for (int i = 0; i < 1000; ++i)
records.Add(new Record()
{
{ "Name", "John " + i },
{ "Surname", "Doe " + i }
});
this.dataGrid.ItemsSource = records;
}
private void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
var property = e.PropertyDescriptor as Property;
if (property != null)
{
var binding = new Binding() { Path = new PropertyPath(property), Mode = property.IsReadOnly ? BindingMode.OneWay : BindingMode.TwoWay };
var dataGridBoundColumn = e.Column as DataGridBoundColumn;
if (dataGridBoundColumn != null)
dataGridBoundColumn.Binding = binding;
else
{
var dataGridComboBoxColumn = e.Column as DataGridComboBoxColumn;
if (dataGridComboBoxColumn != null)
dataGridComboBoxColumn.SelectedItemBinding = binding;
}
}
}
}
public sealed class Record : INotifyPropertyChanged, IEnumerable
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly IDictionary<string, object> values = new SortedList<string, object>(StringComparer.Ordinal);
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, e);
}
public object GetValue(string name)
{
object value;
return this.values.TryGetValue(name, out value) ? value : null;
}
public void SetValue(string name, object value)
{
if (!object.Equals(this.GetValue(name), value))
{
this.values[name] = value;
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
public void Add(string name, object value)
{
this.values[name] = value;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.values.GetEnumerator();
}
}
public sealed class Property : PropertyDescriptor
{
private readonly Type propertyType;
private readonly bool isReadOnly;
public Property(string name)
: this(name, typeof(string))
{
}
public Property(string name, Type propertyType)
: this(name, propertyType, false)
{
}
public Property(string name, Type propertyType, bool isReadOnly, params Attribute[] attributes)
: base(name, attributes)
{
this.propertyType = propertyType;
this.isReadOnly = isReadOnly;
}
public override Type ComponentType
{
get { return typeof(Record); }
}
public override Type PropertyType
{
get { return this.propertyType; }
}
public override bool IsReadOnly
{
get { return this.isReadOnly; }
}
public override object GetValue(object component)
{
var record = component as Record;
return record != null ? record.GetValue(this.Name) : null;
}
public override void SetValue(object component, object value)
{
var record = component as Record;
if (record != null)
record.SetValue(this.Name, value);
}
public override bool CanResetValue(object component)
{
throw new NotSupportedException();
}
public override void ResetValue(object component)
{
throw new NotSupportedException();
}
public override bool ShouldSerializeValue(object component)
{
throw new NotSupportedException();
}
}
public sealed class RecordCollection : ObservableCollection<Record>, ITypedList
{
private readonly PropertyDescriptorCollection properties;
public RecordCollection(params Property[] properties)
{
this.properties = new PropertyDescriptorCollection(properties);
}
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
{
return this.properties;
}
string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
{
return string.Empty;
}
}
}
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication">
<DataGrid x:Name="dataGrid" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>
The key thing in this code is creating a Binding with a BindingPath that contains a Property instance, instead of a string. This enables a simplification of PropertyDescriptor architecture because ICustomTypeDescriptor is not required anymore.
What do you think about this solution?
Property grid do not show new value of selected object.
For example:
o.val = "1";
pg.SelectedObject = o;
o.val = "2";
pg.Refresh();
The property in property grid is still "1";
It is changing only if you click on this property.
Or like that:
o.val = "1";
pg.SelectedObject = o;
o.val = "2";
pg.SelectedObject = o;
but in this case focus will be changed to PropertyGrid.
As I told you in my comment, your code is not enough to understand your issue. Presented like this it should work. Here is mine that works well:
public class Target
{
private int _myInt = 1;
public int MyInt { set; get; }
public static Target target = new Target();
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Button button = new Button()
{
Text = "Click me",
Dock = DockStyle.Bottom
};
Form form = new Form
{
Controls = {
new PropertyGrid {
SelectedObject = Target.target,
Dock = DockStyle.Fill,
},
button
}
};
button.Click += new EventHandler(button_Click);
Application.Run(form);
}
static void button_Click(object sender, EventArgs e)
{
Target.target.MyInt = 2;
Form form = Form.ActiveForm;
(form.Controls[0] as PropertyGrid).Refresh();
}
}
The call to Refresh() actually rebuilds the grid. Remove it and you will see the change only when you click the property.
Cause you just not gave some code, here is a working example.
Just put a Button and a PropertyGrid onto a form.
using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace WindowsFormsApplication
{
public partial class FormMain : Form
{
Random rand;
MyObject obj;
public FormMain()
{
InitializeComponent();
rand = new Random();
obj = new MyObject();
propertyGrid1.SelectedObject = obj;
}
private void button1_Click(object sender, EventArgs e)
{
obj.MyValue = rand.Next();
obj.IsEnabled = !obj.IsEnabled;
obj.MyText = DateTime.Now.ToString();
propertyGrid1.Refresh();
}
}
public class MyObject : INotifyPropertyChanged
{
private int _MyValue;
public int MyValue
{
get
{
return _MyValue;
}
set
{
_MyValue = value;
NotifyPropertyChanged("MyValue");
}
}
private string _MyText;
public string MyText
{
get
{
return _MyText;
}
set
{
_MyText = value;
NotifyPropertyChanged("MyText");
}
}
private bool _IsEnabled;
public bool IsEnabled
{
get
{
return _IsEnabled;
}
set
{
_IsEnabled = value;
NotifyPropertyChanged("IsEnabled");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}