I am creating a List<> of a Test class, I made. I then convert it to an array and assign it to a propertygrid control.
List<Test> lc_Test;
public class Test : UserControl
{
[Category("lc_Test"), Description("Testing using List<>, for storage."), DisplayName("Images")]
public Image img { get; set; }
}
// Above in Form1(), I create it
// lc_Test = new List<Test>();
private void button7_Click(object sender, EventArgs e)
{
lc_Test.Clear();
lc_Test.Add(new Test());
lc_Test.Add(new Test());
lc_Test[0].img = pb1.Image;
lc_Test[1].img = pb2.Image;
MessageBox.Show(lc_Test.Count.ToString());
pgTest.SelectedObject = lc_Test.ToArray();
}
As seen in the picture, it works:
I am now wondering, if there is any way to change the Display Name of each item. Cause, it names them "(0)" and "(1)". In this test, I'd like to change it to say. "Test Item 0" and "Test Item 1". I need to change the "Help Text" for each item, also. Got to be a way.
Anyone, need anything from me let me know.
Solution:
Your form1.cs code file:
using System;
using System.Windows.Forms;
namespace PropertyGridSample
{
public class Form1 : System.Windows.Forms.Form
{
Layers layers_test;
internal System.Windows.Forms.PropertyGrid PropertyGrid1;
private System.ComponentModel.Container components = null; //Required designer variable
public Form1()
{
InitializeComponent();
layers_test = new Layers();
PropertyGrid1.SelectedObject = layers_test;
}
// Clean up any resources being used
protected override void Dispose( bool disposing )
{
if (disposing) { if (components != null) { components.Dispose(); } }
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.PropertyGrid1 = new System.Windows.Forms.PropertyGrid();
this.SuspendLayout();
//
// PropertyGrid1
//
this.PropertyGrid1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.PropertyGrid1.LineColor = System.Drawing.SystemColors.ScrollBar;
this.PropertyGrid1.Location = new System.Drawing.Point(0, -1);
this.PropertyGrid1.Name = "PropertyGrid1";
this.PropertyGrid1.PropertySort = System.Windows.Forms.PropertySort.Alphabetical;
this.PropertyGrid1.Size = new System.Drawing.Size(408, 254);
this.PropertyGrid1.TabIndex = 1;
this.PropertyGrid1.ToolbarVisible = false;
this.PropertyGrid1.SelectedGridItemChanged += new System.Windows.Forms.SelectedGridItemChangedEventHandler(this.PropertyGrid1_SelectedGridItemChanged);
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(408, 254);
this.Controls.Add(this.PropertyGrid1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "Form1";
this.Text = "Customizing Collections in Property Grid Demo";
this.ResumeLayout(false);
}
#endregion
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void PropertyGrid1_SelectedGridItemChanged(object sender, SelectedGridItemChangedEventArgs e)
{
PropertyGrid1.Refresh();
}
}
}
I made a separate cs file, called LayerCollection.cs:
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
namespace PropertyGridSample
{
[TypeConverter(typeof(LayerConverter))]
public class Layer
{
public Image image { get; set; }
public Layer() { }
}
public class Layers
{
LayerCollection layercollection = new LayerCollection();
public Layers()
{
Layer[] layz = new Layer[2];
Layer lay1 = new Layer(); Layer lay2 = new Layer(); //Create two test layers and add them to the layer collection
layercollection.Add(lay1); layercollection.Add(lay2);
}
[TypeConverter(typeof(LayerCollectionConverter))]
public LayerCollection Layer_Collection { get { return layercollection; } }
}
public class LayerCollection : CollectionBase, ICustomTypeDescriptor
{
#region collection impl
public void Add(Layer lay) { List.Add(lay); } //Adds a layer object to the collection
public void Remove(Layer lay) { List.Remove(lay); } //Removes a layer object from the collection
public Layer this[int index] { get { return (List.Count > -1 && index < List.Count) ? (Layer)List[index] : null; } } //Return a layer object at index position
#endregion
#region ICustomTypeDescriptor impl
public AttributeCollection GetAttributes() { return TypeDescriptor.GetAttributes(this, true); }
public String GetClassName() { return TypeDescriptor.GetClassName(this, true); }
public String GetComponentName() { return TypeDescriptor.GetComponentName(this, true); }
public TypeConverter GetConverter() { return TypeDescriptor.GetConverter(this, true); }
public EventDescriptor GetDefaultEvent() { return TypeDescriptor.GetDefaultEvent(this, true); }
public PropertyDescriptor GetDefaultProperty() { return TypeDescriptor.GetDefaultProperty(this, true); }
public EventDescriptorCollection GetEvents(Attribute[] attributes) { return TypeDescriptor.GetEvents(this, attributes, true); }
public EventDescriptorCollection GetEvents() { return TypeDescriptor.GetEvents(this, true); }
public object GetEditor(Type editorBaseType) { return TypeDescriptor.GetEditor(this, editorBaseType, true); }
public object GetPropertyOwner(PropertyDescriptor pd) { return this; }
//Called to get the properties of this type. Returns properties with certain attributes. this restriction is not implemented here.
public PropertyDescriptorCollection GetProperties(Attribute[] attributes) { return GetProperties(); }
//Called to get the properties of this type.
public PropertyDescriptorCollection GetProperties()
{
PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null); // Create a collection object to hold property descriptors
// Iterate the list of layers and create a property descriptor for each layer item and add to the property descriptor collection
for (int i = 0; i < this.List.Count; i++) { LayerCollectionPropertyDescriptor pd = new LayerCollectionPropertyDescriptor(this, i); pds.Add(pd); }
return pds; // return the descriptor collection
}
#endregion
}
class LayerConverter : ExpandableObjectConverter
{
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destType)
{
return (destType == typeof(string) && value is Layer) ? "Layer Data": base.ConvertTo(context, culture, value, destType);
}
}
class LayerCollectionConverter : ExpandableObjectConverter
{
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destType)
{
return (destType == typeof(string) && value is LayerCollection) ? "Items": base.ConvertTo(context, culture, value, destType);
}
}
public class LayerCollectionPropertyDescriptor : PropertyDescriptor
{
private LayerCollection collection = null;
private int index = -1;
public LayerCollectionPropertyDescriptor(LayerCollection coll, int idx) : base("#" + idx.ToString(), null)
{
collection = coll; index = idx;
}
public override AttributeCollection Attributes { get { return new AttributeCollection(null); } }
public override bool CanResetValue(object component) { return true; }
public override bool IsReadOnly { get { return false; } }
public override bool ShouldSerializeValue(object component) { return true; }
public override string Description { get { return "Layer Description"; } }
public override string DisplayName { get { return "Layer " + index.ToString(); } }
public override string Name { get { return "#" + index.ToString(); } }
public override object GetValue(object component) { return collection[index]; }
public override Type ComponentType { get { return collection.GetType(); } }
public override Type PropertyType { get { return collection[index].GetType(); } }
public override void ResetValue(object component) { }
public override void SetValue(object component, object value) { } // this.collection[index] = value;
}
}
On your form, you just need to throw a propertygrid on it. I'm still looking at ways to improve it and need the ability to trap the add and remove buttons, on the built in collection editor
I'm building a Windows Forms Application that displays a custom class Record objects and sorts them by how long they've been in my SortableBindingList<Record> record_list. When I start my program, I have some "dummy" records loaded into this list already for the sake of testing.
The SortableBindingList<T> has been taken from here.
public partial class Form1 : Form
{
public SortableBindingList<Record> record_list = new SortableBindingList<Record> { };
public static DataGridViewCellStyle style = new DataGridViewCellStyle();
public Form1()
{
InitializeComponent();
dataGridView.DataSource = record_list;
FillData(); //Temporary function to insert dummy data for demo.
dataGridView.CellFormatting += new System.Windows.Forms.DataGridViewCellFormattingEventHandler(this.cell_formatting);
this.Controls.Add(dataGridView);
this.dataGridView.RowHeadersVisible = false;
this.dataGridView.Sort(this.dataGridView.Columns["UserName"], ListSortDirection.Ascending);
start_timer();
}
Result before "new" data is added (note: this was alphabetized automatically, specifically entered into the list out of alphabetical order):
Result after data is added:
Finally, result after I click the "UserName" header:
So, must I force a sort every time my DataSource is updated? If that's the case, how do I call a sort in such a manner?
Thank you for your assistance in advance!
You need to apply sort when the list changes.
The SortableBindingList<T> needs some changes to keep the the list sorted when some changes made in list. Here is the full code with changes which I made.
Pay attention The OnListChanged method of BindingList will be called automatically after adding and removing items. But if you need to OnListChanged also runs after changing properties of items, you should implement INotifyPropertyChanged for your model class.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
public class SortableBindingList<T> : BindingList<T>
{
private bool isSortedValue;
ListSortDirection sortDirectionValue;
PropertyDescriptor sortPropertyValue;
public SortableBindingList() : base() { }
public SortableBindingList(IList<T> list) : base(list) { }
protected override void ApplySortCore(PropertyDescriptor prop,
ListSortDirection direction)
{
Type interfaceType = prop.PropertyType.GetInterface("IComparable");
if (interfaceType == null && prop.PropertyType.IsValueType)
{
Type underlyingType = Nullable.GetUnderlyingType(prop.PropertyType);
if (underlyingType != null)
{
interfaceType = underlyingType.GetInterface("IComparable");
}
}
if (interfaceType != null)
{
sortPropertyValue = prop;
sortDirectionValue = direction;
IEnumerable<T> query = base.Items;
if (direction == ListSortDirection.Ascending)
query = query.OrderBy(i => prop.GetValue(i));
else
query = query.OrderByDescending(i => prop.GetValue(i));
int newIndex = 0;
foreach (object item in query)
{
this.Items[newIndex] = (T)item;
newIndex++;
}
isSortedValue = true;
sorting = true;
this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
sorting = false;
}
else
{
throw new NotSupportedException("Cannot sort by " + prop.Name +
". This" + prop.PropertyType.ToString() +
" does not implement IComparable");
}
}
bool sorting = false;
protected override PropertyDescriptor SortPropertyCore
{
get { return sortPropertyValue; }
}
protected override ListSortDirection SortDirectionCore
{
get { return sortDirectionValue; }
}
protected override bool SupportsSortingCore
{
get { return true; }
}
protected override bool IsSortedCore
{
get { return isSortedValue; }
}
protected override void RemoveSortCore()
{
isSortedValue = false;
sortPropertyValue = null;
}
protected override void OnListChanged(ListChangedEventArgs e)
{
if (!sorting && sortPropertyValue != null)
ApplySortCore(sortPropertyValue, sortDirectionValue);
else
base.OnListChanged(e);
}
}
This is the simplest ever custom property editor that contains just a form with one more PropertyGrid:
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms.Design;
namespace PageControls
{
public partial class PropertyGridEditor : Form
{
public object ObjectToEdit;
public delegate void PropertyValueChangedEventHandler(object sender, PropertyValueChangedEventArgs e);
public static event PropertyValueChangedEventHandler PropertyValueChangedStatic;
public event EventHandler<PropertyValueChangedEventArgs> PropertyValueChanged;
public PropertyGridEditor(object obj_to_edit)
{
InitializeComponent();
this.ObjectToEdit = obj_to_edit;
}
private void PropertyGridEditor_Load(object sender, EventArgs e)
{
this.prop_grid.SelectedObject = ObjectToEdit;
}
private void PropertyGridEditor_FormClosed(object sender, FormClosedEventArgs e)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
private void prop_grid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
var evt = PropertyGridEditor.PropertyValueChangedStatic;
if (evt != null)
evt(s, e);
var evt2 = this.PropertyValueChanged;
if (evt2 != null)
evt2(s, e);
}
}
[System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
public class InnerPropertyGridEditor : UITypeEditor
{
public InnerPropertyGridEditor()
{
}
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
// Indicates that this editor can display a Form-based interface.
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
// Attempts to obtain an IWindowsFormsEditorService.
IWindowsFormsEditorService edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (edSvc == null)
return null;
using (PropertyGridEditor form = new PropertyGridEditor(value)) //when two or more properties were selected the value is null :/
if (edSvc.ShowDialog(form) == DialogResult.OK)
return form.ObjectToEdit;
return value; // If OK was not pressed, return the original value
}
}
}
So, now I have a class:
class Test
{
public bool Prop1 { get; set; }
public bool Prop2 { get; set; }
}
And I have main class that has this Test class as property.
class MainClass
{
[Editor(typeof(InnerPropertyGridEditor), typeof(UITypeEditor))]
public Test test_prop { get; set; }
...
}
My main PropertyEditor supports multi selected objects.
So, I can select two or more MainClasses to edit their properties.
The problem is - when I do that and tries to edit test_prop InnerPropertyGridEditor appears empty, because of passed value is null.
Actually, I hoped it to be at least object[] so I can implement something.
Ok, in case if no one will ever answer this I will show the hacky solution I made:
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms.Design;
using System.Reflection;
namespace PageControls
{
public partial class PropertyGridEditor : Form
{
public object Result;
public static event EventHandler<PropertyValueChangedEventArgs> PropertyValueChangedStatic;
public event EventHandler<PropertyValueChangedEventArgs> PropertyValueChanged;
public PropertyGridEditor(object[] obj_to_edit)
{
InitializeComponent();
this.prop_grid.SelectedObjects = obj_to_edit;
this.Result = obj_to_edit[0];
}
private void PropertyGridEditor_Load(object sender, EventArgs e)
{
}
private void PropertyGridEditor_FormClosed(object sender, FormClosedEventArgs e)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
private void prop_grid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
var evt = PropertyGridEditor.PropertyValueChangedStatic;
if (evt != null)
evt(s, e);
var evt2 = this.PropertyValueChanged;
if (evt2 != null)
evt2(s, e);
}
}
[System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
public class InnerPropertyGridEditor : UITypeEditor
{
public InnerPropertyGridEditor()
{
}
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
// Indicates that this editor can display a Form-based interface.
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
// Attempts to obtain an IWindowsFormsEditorService.
IWindowsFormsEditorService edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
if (edSvc == null)
return null;
object[] values = new object[context.Instance is object[] ? ((object[])context.Instance).Length : 1];
if (context.Instance is object[])
for (int i = 0; i < ((object[])context.Instance).Length; i++)
{
PropertyInfo pi = ((object[])context.Instance)[i].GetType().GetProperty(context.PropertyDescriptor.Name);
values[i] = pi != null ? pi.GetValue(((object[])context.Instance)[i], null) : null;
}
else
values[0] = value;
using (PropertyGridEditor form = new PropertyGridEditor(values))
if (edSvc.ShowDialog(form) == DialogResult.OK)
return form.Result;
return value; // If OK was not pressed, return the original value
}
}
}
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?
i have this code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Collections.ObjectModel;
namespace Bix
{
public class SettingsDataObject
{
private int id;
public int Id
{
get { return id; }
set { id = value == 0 ? Db.GetNextSettingsId() : value; }
}
private string adminEmail; public string AdminEmail {
get { return adminEmail; }
set { adminEmail = value; }
}
private int state; public int State { get { return state; } set { state = value == 0 ? 1 : value; } }
public object[] GetArray()
{
return new object[] { id, adminEmail, state };
}
public SettingsDataObject()
{
}
}
public class SettingsUIObjects : ObservableCollection<SettingsUIObject>,INotifyPropertyChanged
{
protected override void InsertItem(int index, SettingsUIObject item)
{
base.InsertItem(index, item);
// handle any EndEdit events relating to this item
item.ItemEndEdit += new SettingsUIObject.ItemEndEditEventHandler(ItemEndEditHandler);
item.PropertyChanged += new SettingsUIObject.PropertyChangedEventHandler(PropertyChanged);
}
public void ItemEndEditHandler(IEditableObject sender)
{
// simply forward any EndEdit events
if (ItemEndEdit != null)
{
ItemEndEdit(sender);
}
}
public event SettingsUIObject.ItemEndEditEventHandler ItemEndEdit;
public event SettingsUIObject.PropertyChangedEventHandler PropertyChanged;
}
public class SettingsDataProvider
{
private DataAccessLayer dl;
public SettingsDataProvider()
{
dl = new DataAccessLayer();
}
public SettingsUIObjects GetSettings()
{
try
{
SettingsUIObjects objs = new SettingsUIObjects();
List<SettingsDataObject> objDataObjects = dl.GetSettings();
foreach (SettingsDataObject obj in objDataObjects)
{
objs.Add(new SettingsUIObject(obj));
}
objs.ItemEndEdit += new SettingsUIObject.ItemEndEditEventHandler(SettingsItemEndEdit);
objs.CollectionChanged += new
NotifyCollectionChangedEventHandler(SettingsCollectionChanged);
objs.PropertyChanged += new SettingsUIObject.PropertyChangedEventHandler(SettingsPropertyChanged);
return objs;
}
catch (Exception) { return new SettingsUIObjects(); }
}
void SettingsItemEndEdit(IEditableObject sender)
{
SettingsUIObject obj = sender as SettingsUIObject;
// use the data access layer to update the wrapped data object
dl.UpdateSettings(obj.GetDataObject());
}
void SettingsPropertyChanged(INotifyPropertyChanged sender, PropertyChangedEventArgs e)
{
SettingsUIObject obj = sender as SettingsUIObject;
// use the data access layer to update the wrapped data object
dl.UpdateSettings(obj.GetDataObject());
}
void SettingsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (object item in e.OldItems)
{
SettingsUIObject obj = item as SettingsUIObject;
// use the data access layer to delete the wrapped data object
dl.DeleteSettings(obj.GetDataObject());
}
}
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (object item in e.NewItems)
{
SettingsUIObject obj = item as SettingsUIObject;
// use the data access layer to delete the wrapped data object
dl.UpdateSettings(obj.GetDataObject());
}
}
}
}
public class SettingsUIObject : IEditableObject, INotifyPropertyChanged
{
private SettingsDataObject obj;
public SettingsUIObject(SettingsDataObject o)
{
obj = o;
}
public SettingsDataObject GetDataObject()
{
return obj;
}
public int Id { get { return obj.Id; } set { obj.Id = value; } }
public string AdminEmail {
get { return obj.AdminEmail; }
set { obj.AdminEmail = value; }
}
public delegate void ItemEndEditEventHandler(IEditableObject sender);
public event ItemEndEditEventHandler ItemEndEdit;
#region IEditableObject Members
public void BeginEdit() { }
public void CancelEdit() { }
public void EndEdit()
{
if (ItemEndEdit != null)
{
ItemEndEdit(this);
}
}
#endregion
public delegate void PropertyChangedEventHandler(INotifyPropertyChanged sender, PropertyChangedEventArgs e);
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
and i keep getting the compile error:
'Bix.SettingsUIObject' does not implement interface member 'System.ComponentModel.INotifyPropertyChanged.PropertyChanged'. 'Bix.SettingsUIObject.PropertyChanged' cannot implement 'System.ComponentModel.INotifyPropertyChanged.PropertyChanged' because it does not have the matching return type of 'System.ComponentModel.PropertyChangedEventHandler'
can anyone tell me why?
thanks
Orson
public delegate void PropertyChangedEventHandler(INotifyPropertyChanged sender, PropertyChangedEventArgs e);
public event PropertyChangedEventHandler PropertyChanged;
Your code redeclares a PropertyChangedEventHandler delegate, which hides the one declared in System.ComponentModel. So your event is of type SettingsUIObject.PropertyChangedEventHandler, not System.ComponentModel.PropertyChangedEventHandler. Since the type doesn't match the one declared in INotifyPropertyChanged, your PropertyChanged event doesn't a valid implementation of the interface.
Just remove your PropertyChangedEventHandler delegate and it should work fine.