4

私は(Winformsから)WPFを初めて使用します。私は.Net 4.5と、WPFのフレームワークに付属するデフォルトのDataGridを使用しています。コンパイル時にはわからないため、列は動的に作成されます。現在、データに基づいて、一部の列は読み取り専用になり、一部は ComboBox タイプになります。

  • 以下に示すように、列を動的に作成しながら、このロジックを動的に適用するにはどうすればよいですか。ここに私がこれまでに書いたコードがあります。データが変更されるたびに、データに基づいて列が動的に生成されます。
  • また、データに基づいて「さまざまなタイプ」の列 (ComboBox、TextBox など) を動的に生成するにはどうすればよいですか。私はテンプレートについてあまり知識がないので、WPFのMVVM っぽい方法は私を制限しています。乗り越えたらきっと楽になるはずです。

注意: 現在、これはすべて正常に機能しています。読み取り専用のデータバインド グリッドがあります。ただし、選択的な編集可能な列と選択的な 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

WPF DataGridは、データソースプロパティタイプがEnumから派生し、プロパティにパブリックセッターがない場合、またはプロパティにReadOnlyAttribute.IsReadOnly = trueがある場合、デフォルトでDataGridColumn.IsReadyOnlyを設定する場合、デフォルトでDataGridComboBoxColumnを作成します。

ここで、データソースのプロパティが上記のデフォルト条件を満たさない場合にDataGrid列生成をカスタマイズする方法を示します。

最初に、プロパティが読み取り専用(EditableAttribute)であり、そのプロパティが事前定義されたドロップダウン項目(NameValueAttribute)を持つComboBoxとして視覚化されることを指定するために使用される2つの属性を紹介します。

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

次に、デモンストレーションに使用するいくつかのサンプルクラスが必要です。

これが、DataGridの単一のアイテム(行)を表すPerson.csクラスです。

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を生成した後に発生し、自動生成された列ごとに1回発生します。提供されたデータソースプロパティに応じて、自動生成された列をカスタマイズしたり、別の列を指定したりするために使用されます。

これが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は、データ項目に実装されたICustomTypeDescriptorとコレクションに実装されたITypedListを使用して動的リフレクションをサポートします。また、.NET 4.5はICustomTypeProviderをサポートしていますが、.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;
        }
    }
}
于 2012-12-14T21:05:56.573 に答える
1

私は数日間インターネットから離れていましたが、ICustomTypeDescriptorを実装する必要のない単純化されたPropertyDescriptorアーキテクチャを使用したより良いアプローチを見つけたと思います。コード全体は次のとおりです。

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を作成することです。これにより、ICustomTypeDescriptorが不要になったため、PropertyDescriptorアーキテクチャを簡素化できます。

このソリューションについてどう思いますか?

于 2012-12-29T01:27:37.353 に答える
1

まず、WinForms に対する WPF の主な利点の 1 つは、テンプレートを使用してユーザー インターフェイスを宣言できることです。また、コード内で 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 に答える