WPF:Datagrid - 动态应用DataGridTemplateColumn.CellTemplate

时间:2012-12-14 14:28:29

标签: c# wpf

我是WPF的新手(来自Winforms)。我正在使用.Net 4.5和WPF中随框架一起提供的默认DataGrid。列是动态创建的,因为我在编译时不知道。现在,基于数据,一些列将是只读的,一些列将是ComboBox类型。

  • 如何动态创建列时如何应用this logic dynamically,如下所示。这是我到目前为止写的代码。只要数据发生变化,就会根据数据动态生成列。
  • 另外,如何根据数据动态生成“不同类型”的列(ComboBox,TextBox等)。 WPF中的 MVVM-ish方式有点限制我,因为我对模板没有太多了解。我确信一旦通过就应该很容易。

注意:目前这一切都运转良好。我有一个只读的数据绑定网格。但是,不支持选择性可编辑列和选择性ComboBox列。

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

4 个答案:

答案 0 :(得分:2)

如果数据源属性类型派生自DataGridComboBoxColumn,则默认情况下WPF DataGrid会创建Enum,如果属性没有公共setter或属性为{{3},则默认设置DataGridColumn.IsReadyOnly } ReadOnlyAttribute = true。

如果您的数据源属性不满足上述默认条件,我现在将展示如何自定义DataGrid列生成。

首先,我将介绍两个用于指定属性是只读的属性(EditableAttribute),并且该属性应该可视化为具有预定义下拉项的ComboBox(NameValueAttribute)。

这是 EditableAttribute.cs

using System;

namespace WpfApplication
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public sealed class EditableAttribute : Attribute
    {
        public bool AllowEdit { get; set; }
    }
}

这是 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; }
    }
}

接下来,我们需要一些将用于演示的示例类。

所以这里是 Person.cs 类,它将代表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"); }
        }
    }
}

注意Surname属性如何应用EditableAttribute并且Gender属性应用了NameValueAttributes。

以下是代表DataGrid数据源的 People.cs 类:

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'
                });
        }
    }
}

Person的基类是 ObservableObject.cs ,这对所有数据绑定应用程序都是通用的:

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));
            }
        }
    }
}

现在,这是承载DataGrid控件的 MainWindow.xaml 的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">
    <Window.Resources>
        <local:People x:Key="itemsSource"/>
    </Window.Resources>
    <DataGrid ItemsSource="{StaticResource itemsSource}" AutoGeneratingColumn="OnAutoGeneratingColumn"/>
</Window>

关键部分是ReadOnlyAttribute.IsReadOnly事件处理程序OnAutoGeneratingColumn。 DataGrid生成DataGridColumn后会触发此事件,并且会为每个自动生成的列触发一次。它用于自定义自动生成的列或指定不同的列,具体取决于DataGrid.AutoGeneratingColumn

这是 MainWindow.xaml.cs 代码隐藏,其中OnAutoGeneratingColumn事件处理程序就是这样做的。如果数据源属性具有带AllowEdit = false的EditableAttribute,则通过将其设置为只读来自定义生成列,如果数据源属性具有NameValueAttributes,则它将使用DataGridComboBoxColumn覆盖自动生成的列:

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;
        }
    }
}

动态案例更新:

WPF支持动态反射,provided data source property在数据项上实现,ICustomTypeDescriptor在集合上实现。 此外,.NET 4.5支持ITypedList,但由于我没有安装.NET 4.5,我还没有测试过。

NameValueAttribute.cs 与之前相同。

以下是工作示例中ICustomTypeDescriptor和ITypedList的非常简单的实现:

<强> 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;
        }
    }
}

答案 1 :(得分:1)

首先,WPF到WinForms的主要优点之一是能够使用模板声明用户界面。并且您应该尽可能避免在代码中声明UI组件

据我了解,您希望根据对象类型/数据显示不同对象的集合。 实现此类逻辑的最佳方式 - 实现您自己的TemplateSelector

我建议你阅读下一篇文章:

  1. http://www.wpftutorial.net/DataGrid.html
  2. http://www.switchonthecode.com/tutorials/wpf-tutorial-how-to-use-a-datatemplateselector
  3. P.S。 以供参考。在代码中声明DataTemplate的示例:

    //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;
    

    但正如我上面所说,你应该避免这种情况。

答案 2 :(得分:1)

这是正确答案 - http://www.paulstovell.com/dynamic-datagrid(动态参见模板创建逻辑。它很聪明)。

并且,MMVM将会像这样实现 - http://www.codeproject.com/Articles/36462/Binding-a-ListView-to-a-Data-Matrix(几乎我在问题中发布的内容)

答案 3 :(得分:1)

我离开互联网已有几天了,但我认为我已经找到了更简单的PropertyDescriptor架构的方法,它不需要实现ICustomTypeDescriptor。这是整个代码:

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>

此代码中的关键是使用包含Property实例的BindingPath创建Binding,而不是字符串。这样可以简化PropertyDescriptor体系结构,因为不再需要ICustomTypeDescriptor。

您对此解决方案有何看法?