私の質問: DataTable のセルを複雑なオブジェクトにバインドするにはどうすればよいですか? さらに、グリッド ビューでは、複雑なオブジェクトのプロパティを 1 つだけ編集したいと考えています。
ここで必要なのは、いわゆるプロパティ パス(例: obj.Prop1.Prop2
) にバインドする機能です。残念ながら、WinForms はそのサポートを制限しています。単純なデータ バインディング ( などcontrol.DataBindings.Add(...)
) ではサポートされていますが、コントロールなど で使用されるリストデータ バインディングではサポートされていません。DataGridView
幸いなことに、データバインディングは と呼ばれる抽象化を中心に構築されているため、一部の (ほとんどの場合は些細な) コーディングでまだ実行可能PropertyDescriptor
です。デフォルトでは、リフレクションを介して実装されますが、独自の実装を作成し、その中で好きなことを行うことを妨げるものは何もありません。これにより、特に実際には存在しない「プロパティ」をシミュレートするために、リフレクションでは不可能な多くのことを行うことができます。
ここでは、その可能性を利用して、元のプロパティの子プロパティから実際に値を取得/設定する「プロパティ」を作成しますが、外部からはまだ単一のプロパティのように見えるため、データバインドを可能にします。
public class ChildPropertyDescriptor : PropertyDescriptor
{
public static PropertyDescriptor Create(PropertyDescriptor sourceProperty, string childPropertyPath, string displayName = null)
{
var propertyNames = childPropertyPath.Split('.');
var propertyPath = new PropertyDescriptor[1 + propertyNames.Length];
propertyPath[0] = sourceProperty;
for (int i = 0; i < propertyNames.Length; i++)
propertyPath[i + 1] = propertyPath[i].GetChildProperties()[propertyNames[i]];
return new ChildPropertyDescriptor(propertyPath, displayName);
}
private ChildPropertyDescriptor(PropertyDescriptor[] propertyPath, string displayName)
: base(propertyPath[0].Name, null)
{
this.propertyPath = propertyPath;
this.displayName = displayName;
}
private PropertyDescriptor[] propertyPath;
private string displayName;
private PropertyDescriptor RootProperty { get { return propertyPath[0]; } }
private PropertyDescriptor ValueProperty { get { return propertyPath[propertyPath.Length - 1]; } }
public override Type ComponentType { get { return RootProperty.ComponentType; } }
public override bool IsReadOnly { get { return ValueProperty.IsReadOnly; } }
public override Type PropertyType { get { return ValueProperty.PropertyType; } }
public override bool CanResetValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.CanResetValue(target); }
public override object GetValue(object component) { var target = GetTarget(component); return target != null ? ValueProperty.GetValue(target) : null; }
public override void ResetValue(object component) { ValueProperty.ResetValue(GetTarget(component)); }
public override void SetValue(object component, object value) { ValueProperty.SetValue(GetTarget(component), value); }
public override bool ShouldSerializeValue(object component) { var target = GetTarget(component); return target != null && ValueProperty.ShouldSerializeValue(target); }
public override AttributeCollection Attributes { get { return ValueProperty.Attributes; } }
public override string Category { get { return ValueProperty.Category; } }
public override TypeConverter Converter { get { return ValueProperty.Converter; } }
public override string Description { get { return ValueProperty.Description; } }
public override bool IsBrowsable { get { return ValueProperty.IsBrowsable; } }
public override bool IsLocalizable { get { return ValueProperty.IsLocalizable; } }
public override string DisplayName { get { return displayName ?? RootProperty.DisplayName; } }
public override object GetEditor(Type editorBaseType) { return ValueProperty.GetEditor(editorBaseType); }
public override PropertyDescriptorCollection GetChildProperties(object instance, Attribute[] filter) { return ValueProperty.GetChildProperties(GetTarget(instance), filter); }
public override bool SupportsChangeEvents { get { return ValueProperty.SupportsChangeEvents; } }
public override void AddValueChanged(object component, EventHandler handler)
{
var target = GetTarget(component);
if (target != null)
ValueProperty.AddValueChanged(target, handler);
}
public override void RemoveValueChanged(object component, EventHandler handler)
{
var target = GetTarget(component);
if (target != null)
ValueProperty.RemoveValueChanged(target, handler);
}
private object GetTarget(object source)
{
var target = source;
for (int i = 0; target != null && target != DBNull.Value && i < propertyPath.Length - 1; i++)
target = propertyPath[i].GetValue(target);
return target != DBNull.Value ? target : null;
}
}
コードはそれほど小さくはありませんが、基本的には、元のプロパティから子プロパティへのパスを表すプロパティ記述子チェーンの対応するメソッドへの呼び出しを委譲するだけです。PropertyDescriptor
また、 の多くのメソッドは設計時にのみ使用されることに注意してください。そのため、カスタムの具体的なランタイム プロパティ記述子を作成するには、通常ComponentType
、 、PropertyType
、GetValue
およびSetValue
(サポートされている場合) を実装するだけで済みます。
ここまでは順調ですね。これはパズルの最初の部分にすぎません。「プロパティ」を作成できますが、データバインディングでそれを使用できるようにする方法が必要です。
そのために、次のデータ バインディング関連の別のインターフェイスを使用しますITypedList
。
バインド可能なリストのスキーマを検出する機能を提供します。バインドに使用できるプロパティは、バインド先のオブジェクトのパブリック プロパティとは異なります。
つまり、リスト要素に「プロパティ」を提供できます。しかし、どのように?データ ソース リストを実装していれば、簡単です。しかし、ここでは、事前にわからないリストに対してそれを行いたいと考えています (解決策を一般的なものにしようとしています)。
IList
解決策は、基になるリストへのすべての呼び出しを委譲することによって (リスト データ バインディングの最小要件) を実装する別のリストで元のリストをラップすることですが、実装によってITypedList
バインディングに使用されるプロパティを制御します。
public static class ListDataView
{
public static IList Create(object dataSource, string dataMember, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper)
{
var source = (IList)ListBindingHelper.GetList(dataSource, dataMember);
if (source == null) return null;
if (source is IBindingListView) return new BindingListView((IBindingListView)source, propertyMapper);
if (source is IBindingList) return new BindingList((IBindingList)source, propertyMapper);
return new List(source, propertyMapper);
}
private class List : IList, ITypedList
{
private readonly IList source;
private readonly Func<PropertyDescriptor, PropertyDescriptor> propertyMapper;
public List(IList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) { this.source = source; this.propertyMapper = propertyMapper; }
// IList
public object this[int index] { get { return source[index]; } set { source[index] = value; } }
public int Count { get { return source.Count; } }
public bool IsFixedSize { get { return source.IsFixedSize; } }
public bool IsReadOnly { get { return source.IsReadOnly; } }
public bool IsSynchronized { get { return source.IsSynchronized; } }
public object SyncRoot { get { return source.SyncRoot; } }
public int Add(object value) { return source.Add(value); }
public void Clear() { source.Clear(); }
public bool Contains(object value) { return source.Contains(value); }
public void CopyTo(Array array, int index) { source.CopyTo(array, index); }
public IEnumerator GetEnumerator() { return source.GetEnumerator(); }
public int IndexOf(object value) { return source.IndexOf(value); }
public void Insert(int index, object value) { source.Insert(index, value); }
public void Remove(object value) { source.Remove(value); }
public void RemoveAt(int index) { source.RemoveAt(index); }
// ITypedList
public string GetListName(PropertyDescriptor[] listAccessors) { return ListBindingHelper.GetListName(source, listAccessors); }
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
var properties = ListBindingHelper.GetListItemProperties(source, listAccessors);
if (propertyMapper != null)
properties = new PropertyDescriptorCollection(properties.Cast<PropertyDescriptor>()
.Select(propertyMapper).Where(p => p != null).ToArray());
return properties;
}
}
private class BindingList : List, IBindingList
{
private IBindingList source;
public BindingList(IBindingList source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
private ListChangedEventHandler listChanged;
public event ListChangedEventHandler ListChanged
{
add
{
var oldHandler = listChanged;
if ((listChanged = oldHandler + value) != null && oldHandler == null)
source.ListChanged += OnListChanged;
}
remove
{
var oldHandler = listChanged;
if ((listChanged = oldHandler - value) == null && oldHandler != null)
source.ListChanged -= OnListChanged;
}
}
private void OnListChanged(object sender, ListChangedEventArgs e)
{
var handler = listChanged;
if (handler != null)
handler(this, e);
}
public bool AllowNew { get { return source.AllowNew; } }
public bool AllowEdit { get { return source.AllowEdit; } }
public bool AllowRemove { get { return source.AllowRemove; } }
public bool SupportsChangeNotification { get { return source.SupportsChangeNotification; } }
public bool SupportsSearching { get { return source.SupportsSearching; } }
public bool SupportsSorting { get { return source.SupportsSorting; } }
public bool IsSorted { get { return source.IsSorted; } }
public PropertyDescriptor SortProperty { get { return source.SortProperty; } }
public ListSortDirection SortDirection { get { return source.SortDirection; } }
public object AddNew() { return source.AddNew(); }
public void AddIndex(PropertyDescriptor property) { source.AddIndex(property); }
public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { source.ApplySort(property, direction); }
public int Find(PropertyDescriptor property, object key) { return source.Find(property, key); }
public void RemoveIndex(PropertyDescriptor property) { source.RemoveIndex(property); }
public void RemoveSort() { source.RemoveSort(); }
}
private class BindingListView : BindingList, IBindingListView
{
private IBindingListView source;
public BindingListView(IBindingListView source, Func<PropertyDescriptor, PropertyDescriptor> propertyMapper) : base(source, propertyMapper) { this.source = source; }
public string Filter { get { return source.Filter; } set { source.Filter = value; } }
public ListSortDescriptionCollection SortDescriptions { get { return source.SortDescriptions; } }
public bool SupportsAdvancedSorting { get { return source.SupportsAdvancedSorting; } }
public bool SupportsFiltering { get { return source.SupportsFiltering; } }
public void ApplySort(ListSortDescriptionCollection sorts) { source.ApplySort(sorts); }
public void RemoveFilter() { source.RemoveFilter(); }
}
}
実際、ご覧のとおり、IBindingList
や などの他のデータ ソース インターフェイスのラッパーを追加しましIBindingListView
た。繰り返しますが、コードはそれほど小さくはありませんが、基礎となるオブジェクトへの呼び出しを委譲しているだけです (具象データ用にオブジェクトを作成するときは、通常、2 つのメンバーのみを継承List<T>
またはBiundingList<T>
実装します)。ITypedList
重要な部分は、GetItemProperties
メソッドの実装であり、ラムダとともに、propertyMapper
あるプロパティを別のプロパティに置き換えることができます。
これらすべてが整ったら、投稿から特定の問題を解決するのは簡単で、プロパティをプロパティにラップしDataTable
てマッピングするだけです。Complex
Complex.Name
class ComplexData
{
public int Value { get; set; }
}
class ComplexDataWrapper
{
public string Name { get; set; }
public ComplexData Data { get; set; } = new ComplexData();
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new Form();
var gridView = new DataGridView { Dock = DockStyle.Fill, Parent = form };
gridView.DataSource = ListDataView.Create(GetData(), null, p =>
{
if (p.PropertyType == typeof(ComplexDataWrapper))
return ChildPropertyDescriptor.Create(p, "Name", "Complex Name");
return p;
});
Application.Run(form);
}
static DataTable GetData()
{
var dt = new DataTable();
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("Complex", typeof(ComplexDataWrapper));
for (int i = 1; i <= 10; i++)
dt.Rows.Add(i, new ComplexDataWrapper { Name = "Name#" + i, Data = new ComplexData { Value = i } });
return dt;
}
}
要約すると、カスタマイズPropertyDescriptor
しITypedList
て、データの無制限のタイプのビューを作成できるようにします。これは、データ バインド対応コントロールで使用できます。