0

I have a record structure that I am trying to bind to a DataGrid. Basically, my columns are dynamically specified by the view model. The value that should be displayed (and edited) in grid cell must be retrieved using a function on each record (the view model behind each row of the grid).

I looked at this question and it got me half the way there. Unfortunately, I cannot use a DynamicObject because my properties on the record are namespaced names and as such cannot be represented by a string. I tried a variation where I converted this namespaced name into a bindable string (replacing all illegal characters with an underscore) but it seems like the DynamicObject would call TryGetMember every time I navigated the DataGrid (i.e. it would call the function for every cell every time I scrolled). That performance is not going to be good enough.

This is what I have so far that works for displaying data (i.e. GetValue):

public interface IDataSet
{
    IEnumerable<IRecord> Records { get; }
    IEnumerable<IProperty> PropertiesToDisplay { get; } 
}

public interface IProperty
{
    XName Name { get; }
}

public interface IRecord
{
    IEnumerable<KeyValuePair<IProperty, object>> Values { get; set; }

    object GetValue(IProperty property);
    void SetValue(IProperty property, object value);
}

internal class SomeClass
{
    DataGrid myGrid;    // this is defined in the XAML
    IDataSet viewModel; // this is the DataContext

    private void BindToViewModelUsingConverter()
    {
        foreach (IProperty property in viewModel.PropertiesToDisplay)
        {
            Binding binding = new Binding();
            binding.Converter = new ConvertMyDataConverter();
            binding.ConverterParameter = property;

            var column = new DataGridTextColumn
            {
                Header = property.Name.LocalName,
                Binding = binding
            };

            myGrid.Columns.Add(column);
        }

        myGrid.ItemsSource = viewModel.Records;
    }
}

internal class ConvertMyDataConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var record = value as IRecord;
        var property = parameter as IProperty;

        if (record != null && property != null)
        {
            return record.GetValue(property);
        }

        return DependencyProperty.UnsetValue;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // not sure what to do here, yet
        throw new NotImplementedException();
    }
}

The above code works when it comes to showing values. However, as soon as I try to edit values I get an exception:

**Two-way binding requires Path or XPath**

This makes sense because even in my converter above, I'm not sure what I would write in the ConvertBack function since I want the source to remain the same (since I'm binding to IRecord and have already called the IRecord.SetValue function to update the data.) However, it seems like ConvertBack would not give me a reference to IRecord even if I was able to do two-way binding without a path.

Is there something trivial that I am missing? I'm not averse to developing a custom control but would like some tips/ideas on how to approach the problem.

4

1 回答 1

0

我使用ICustomTypeDescriptorand解决了这个问题ITypedList。我没有将其作为Records一个. 对于实现,我返回了包装我的实现的属性描述符:IEnumerable<IRecord>IList<IRecord>ITypedListITypedListIProperty

public interface IDataSet
{
    TypedRecordsCollection Records { get; }
    IEnumerable<IProperty> PropertiesToDisplay { get; } 
}

public class TypedRecordsCollection : ObservableCollection<RecordViewModel>, ITypedList
{
    private PropertyDescriptorCollection _properties;

    public TypedRecordsCollection(IEnumerable<IRecord> records)
    {
        _properties = new PropertyDescriptorCollection(
            PropertiesToDisplay
                .Select(prop => new CustomPropertyDescriptor(prop))
                .ToArray());
        records.ForEach(rec => Items.Add(new RecordViewModel(rec, _properties)));
    }

    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        return _properties;
    }
 }   

 public interface IRecordViewModel : ICustomTypeDescriptor
 {
    object GetCellValue(IProperty property);
    void SetCellValue(IProperty property, object value);
 }

 public class RecordViewModel : CustomTypeDescriptor, IRecord
 {
    private IRecord _record;
    private PropertyDescriptorCollection _properties;

    public RecordViewModel(IRecord record, PropertyDescriptorCollection properties)
    {
        _record = record;
        _properties = properties;
    }

    public object GetCellValue(IProperty property)
    {
        return _record.GetValue(property);
    }

    public void SetCellValue(IProperty property, object value)
    {
        _record.SetValue(property, value);
    }

    public override PropertyDescriptorCollection GetProperties()
    {
        return _properties;
    }
 }

 public class CustomPropertyDescriptor : PropertyDescriptor
 {
    private IProperty _property;

    public CustomPropertyDescriptor(IProperty property)
        : base(GetCleanName(property.Name), new Attribute[0])
    {
        _property = property;
    }

    public override object GetValue(object component)
    {
        var recordViewModel = component as IRecordViewModel;
        if (recordViewModel != null)
        {
            return recordViewModel.GetCellValue(_property);
        }

        return null;
    }

    public override void SetValue(object component, object value)
    {
        var recordViewModel = component as IRecordViewModel;
        if (recordViewModel != null)
        {
            recordViewModel.SetCellValue(_property, value);
        }
    }

    private static string GetCleanName(XName name)
    {
        // need to return a XAML bindable string.. aka no "{}" or ".", etc.
    }
 }

现在在我的 XAML 中,我可以设置AutogenerateColumns为 true 并直接绑定到Records属性而无需转换器。

于 2014-06-30T23:44:07.840 回答