4

我对 WPF(来自 Winforms)很陌生。我正在使用 .Net 4.5 和 WPF 中框架附带的默认 DataGrid。这些列是动态创建的,因为我在编译时不知道。现在,根据数据,一些列将是只读的,而一些列将是 ComboBox 类型。

  • 如何在动态创建列时动态应用此逻辑,如下所示。这是我到目前为止写的代码。每当数据发生变化时,都会根据数据动态生成列。
  • 另外,如何根据数据动态生成“不同类型”的列(组合框、文本框等)。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

4 回答 4

2

如果数据源属性类型派生自Enum ,则WPF DataGrid 默认创建DataGridComboBoxColumn ,如果属性没有公共 setter 或属性具有ReadOnlyAttributeReadOnlyAttribute.IsReadOnly = true ,则默认设置DataGridColumn.IsReadyOnly 。

如果您的数据源属性不满足上述默认条件,我现在将展示如何自定义 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>

关键部分是DataGrid.AutoGeneratingColumn事件处理程序OnAutoGeneratingColumn。此事件在 DataGrid 生成 DataGridColumn 后触发,并为每个自动生成的列触发一次。它用于自定义自动生成的列或指定不同的列,具体取决于提供的数据源属性

这是MainWindow.xaml.cs代码隐藏,其中OnAutoGeneratingColumn事件处理程序正是这样做的。如果数据源属性具有允许编辑的 EditableAttribute = false,它通过将生成的列设置为只读来自定义生成的列,如果数据源属性具有 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 支持在数据项上实现ICustomTypeDescriptor和在集合上实现ITypedList的动态反射。此外,.NET 4.5 支持ICustomTypeProvider,但由于我没有安装 .NET 4.5,所以我没有对其进行测试。

NameValueAttribute.cs和以前一样。

这是一个工作示例中 ICustomTypeDescriptor 和 ITypedList 的非常简单的实现:

数据属性.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
    }
}

数据记录.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
    }
}

数据记录集合.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;
        }
    }
}
于 2012-12-14T21:05:56.573 回答
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

PS 供参考。在代码中声明 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;

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

于 2012-12-14T16:01:45.543 回答
1

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

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

于 2012-12-26T23:56:26.150 回答
1

我离开 Internet 几天了,但我认为我找到了更好的方法,即简化的 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 实例而不是 string 的 BindingPath 创建一个 Binding。这可以简化 PropertyDescriptor 体系结构,因为不再需要 ICustomTypeDescriptor。

您如何看待这个解决方案?

于 2012-12-29T01:27:37.353 回答