2015-09-15 14 views
9

Я хочу автоматически показывать каждый IList как расширяемый в моем PropertyGrid (Под «расширяемым» я, очевидно, подразумеваю, что элементы будут показаны). Я не хочу, чтобы использовать атрибуты в каждом списке (Еще раз, я хочу, чтобы работать на КАЖДОМ IList)PropertyGrid расширяемая коллекция

Я попытался его для того чтобы достигнуть с помощью пользовательского PropertyDescriptor и ExpandableObjectConverter. Он работает, но после удаления элементов из списка PropertyGrid не обновляется, все еще отображая удаленные элементы.

Я попытался использовать ObservableCollection вместе с поднятием OnComponentChanged, а также RefreshProperties атрибут, но ничего не получилось.

Это мой код:

public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor 
{ 
    private IList _collection; 

    private readonly int _index = -1; 

    internal event EventHandler RefreshRequired; 

    public ExpandableCollectionPropertyDescriptor(IList coll, int idx) : base(GetDisplayName(coll, idx), null) 
    { 
     _collection = coll 
     _index = idx; 
    } 

    public override bool SupportsChangeEvents 
    { 
     get { return true; } 
    } 

    private static string GetDisplayName(IList list, int index) 
    { 

     return "[" + index + "] " + CSharpName(list[index].GetType()); 
    } 

    private static string CSharpName(Type type) 
    { 
     var sb = new StringBuilder(); 
     var name = type.Name; 
     if (!type.IsGenericType) 
      return name; 
     sb.Append(name.Substring(0, name.IndexOf('`'))); 
     sb.Append("<"); 
     sb.Append(string.Join(", ", type.GetGenericArguments() 
             .Select(CSharpName))); 
     sb.Append(">"); 
     return sb.ToString(); 
    } 

    public override AttributeCollection Attributes 
    { 
     get 
     { 
      return new AttributeCollection(null); 
     } 
    } 

    public override bool CanResetValue(object component) 
    { 

     return true; 
    } 

    public override Type ComponentType 
    { 
     get 
     { 
      return _collection.GetType(); 
     } 
    } 

    public override object GetValue(object component) 
    { 
     OnRefreshRequired(); 

     return _collection[_index]; 
    } 

    public override bool IsReadOnly 
    { 
     get { return false; } 
    } 

    public override string Name 
    { 
     get { return _index.ToString(); } 
    } 

    public override Type PropertyType 
    { 
     get { return _collection[_index].GetType(); } 
    } 

    public override void ResetValue(object component) 
    { 
    } 

    public override bool ShouldSerializeValue(object component) 
    { 
     return true; 
    } 

    public override void SetValue(object component, object value) 
    { 
     _collection[_index] = value; 
    } 

    protected virtual void OnRefreshRequired() 
    { 
     var handler = RefreshRequired; 
     if (handler != null) handler(this, EventArgs.Empty); 
    } 
} 

.

internal class ExpandableCollectionConverter : ExpandableObjectConverter 
{ 
    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destType) 
    { 
     if (destType == typeof(string)) 
     { 
      return "(Collection)"; 
     } 
     return base.ConvertTo(context, culture, value, destType); 
    } 

    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) 
    { 
     IList collection = value as IList; 
     PropertyDescriptorCollection pds = new PropertyDescriptorCollection(null); 

     for (int i = 0; i < collection.Count; i++) 
     { 
      ExpandableCollectionPropertyDescriptor pd = new ExpandableCollectionPropertyDescriptor(collection, i); 
      pd.RefreshRequired += (sender, args) => 
      { 
       var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance); 
       notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1}); 
      }; 
      pds.Add(pd); 
     } 
     // return the property descriptor Collection 
     return pds; 
    } 
} 

И я использую его для всех IList с со следующей строкой:

TypeDescriptor.AddAttributes(typeof (IList), new TypeConverterAttribute(typeof(ExpandableCollectionConverter))); 

Некоторые Разъяснения

Я хочу, чтобы сетка автоматически обновлять при изменении списка. Обновление, когда меняется другое свойство, не помогает.

Решение, которое работает, это решение, в котором:

  1. Если развернуть список, пока он пуст, а затем добавить элементы, сетка обновляется с деталями расширен
  2. Если вы добавляете элементы в список, разверните его, а затем удалите элементы (без коллапса), сетка обновится с расширенными элементами и не выбрасывает ArgumentOutOfRangeException, потому что пытается показать уже удаленные элементы
  3. Я хочу, чтобы все это для утилита конфигурации. Только PropertyGrid следует изменить коллекции

ВАЖНО EDIT:

мне удалось сделать расширенные коллекции обновления с Reflection и призывающих NotifyValueGivenParent метод на context объекта, когда PropertyDescriptor метод ПолучитьЗначение называется (когда RefreshRequired событие возникает):

var notifyValueGivenParentMethod = context.GetType().GetMethod("NotifyValueGivenParent", BindingFlags.NonPublic | BindingFlags.Instance); 
notifyValueGivenParentMethod.Invoke(context, new object[] {context.Instance, 1}); 

он отлично работает, за исключением того, что вызывает событие чтобы вызвать бесконечное время, потому что вызов NotifyValueGivenParent вызывает перезагрузку PropertyDescriptor и, следовательно, повышение события и т. д.

Я попытался решить эту проблему, добавив простой флаг, который предотвратит перезагрузку, если он уже перезагрузился, но по какой-то причине NotifyValueGivenParent ведет себя асинхронно, и поэтому перезагрузка происходит после выключения флага. Возможно, это еще одно направление для изучения.Единственная проблема заключается в рекурсии

+0

Почему бы вам просто не называть 'TypeDescriptor.AddAttributes (typeof (IList), новый TypeConverterAttribute (typeof (ExpandableObjectConverter)));' вместо вашего пользовательского класса? –

+0

@SimonMourier, потому что тогда я не вижу элементы в коллекции, но свойства 'Capacity' и' Count' –

+0

Это требование не появляется в вашем вопросе. BTW, он работает для меня с свойством типа ArrayList. Я полагаю, это зависит от класса в SelectedObject. вы должны заполнить свой вопрос со всем соответствующим кодом и полным вопросом. –

ответ

2

Нет необходимости использовать ObservableCollection. Вы можете изменить свой класс дескриптора следующим образом:

public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor 
{ 
    private IList collection; 
    private readonly int _index; 

    public ExpandableCollectionPropertyDescriptor(IList coll, int idx) 
     : base(GetDisplayName(coll, idx), null) 
    { 
     collection = coll; 
     _index = idx; 
    } 

    private static string GetDisplayName(IList list, int index) 
    { 
     return "[" + index + "] " + CSharpName(list[index].GetType()); 
    } 

    private static string CSharpName(Type type) 
    { 
     var sb = new StringBuilder(); 
     var name = type.Name; 
     if (!type.IsGenericType) 
      return name; 
     sb.Append(name.Substring(0, name.IndexOf('`'))); 
     sb.Append("<"); 
     sb.Append(string.Join(", ", type.GetGenericArguments() 
             .Select(CSharpName))); 
     sb.Append(">"); 
     return sb.ToString(); 
    } 

    public override bool CanResetValue(object component) 
    { 
     return true; 
    } 

    public override Type ComponentType 
    { 
     get { return this.collection.GetType(); } 
    } 

    public override object GetValue(object component) 
    { 
     return collection[_index]; 
    } 

    public override bool IsReadOnly 
    { 
     get { return false; } 
    } 

    public override string Name 
    { 
     get { return _index.ToString(CultureInfo.InvariantCulture); } 
    } 

    public override Type PropertyType 
    { 
     get { return collection[_index].GetType(); } 
    } 

    public override void ResetValue(object component) 
    { 
    } 

    public override bool ShouldSerializeValue(object component) 
    { 
     return true; 
    } 

    public override void SetValue(object component, object value) 
    { 
     collection[_index] = value; 
    } 
} 

Вместо ExpandableCollectionConverter я бы вывести CollectionConverter класс, так что вы можете использовать кнопку с многоточием, чтобы изменить коллекцию по-старому (так что вы можете добавить/удалить предметы, если коллекция не только для чтения):

public class ListConverter : CollectionConverter 
{ 
    public override bool GetPropertiesSupported(ITypeDescriptorContext context) 
    { 
     return true; 
    } 

    public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) 
    { 
     IList list = value as IList; 
     if (list == null || list.Count == 0) 
     return base.GetProperties(context, value, attributes); 

     var items = new PropertyDescriptorCollection(null); 
     for (int i = 0; i < list.Count; i++) 
     { 
      object item = list[i]; 
      items.Add(new ExpandableCollectionPropertyDescriptor(list, i)); 
     } 
     return items; 
    } 
} 

И я хотел бы использовать эту ListConverter на свойствах, где я хочу видеть расширяемый список. Конечно, вы можете зарегистрировать конвертер типов в целом, как и в своем примере, но это переопределяет все, что может не быть в общем предназначено.

public class MyClass 
{ 
    [TypeConverter(typeof(ListConverter))] 
    public List<int> List { get; set; } 

    public MyClass() 
    { 
     List = new List<int>(); 
    } 

    [RefreshProperties(RefreshProperties.All)] 
    [Description("Change this property to regenerate the List")] 
    public int Count 
    { 
     get { return List.Count; } 
     set { List = Enumerable.Range(1, value).ToList(); } 
    } 
} 

Важно: Атрибут RefreshProperties должен быть определен для свойств, которые изменяют другие свойства. В этом примере изменение Count заменяет весь список.

Используя его как propertyGrid1.SelectedObject = new MyClass(); получается следующий результат:

enter image description here

+2

Это не то, что я хотел. Я не хочу, чтобы он обновлялся, когда обновляется другое свойство. Я хочу, чтобы он обновлялся при изменении списка. Я добавляю элементы в список, разворачиваю его, добавляю больше предметов, но предметы не обновляются –

+0

Спасибо! Отличный пример - именно то, что мне нужно. – Glenn

3

Я не хочу, чтобы обновить, когда другие освежают собственности. Я хочу, чтобы он обновлялся при изменении списка. Я добавляю элементы в список, разворачиваю его, добавляю больше предметов, но предметы не обновляются

Это типичное неправильное использование PropertyGrid. Он предназначен для настройки компонента, а не для отражения параллельных изменений на лету внешним источником. Даже упаковка IList в ObservableCollection не поможет вам, потому что она используется только вашим дескриптором, а внешний источник напрямую манипулирует базовым IList экземпляром.

То, что вы все еще можете сделать это особенно некрасиво хак:

public class ExpandableCollectionPropertyDescriptor : PropertyDescriptor 
{ 
    // Subscribe to this event from the form with the property grid 
    public static event EventHandler CollectionChanged; 

    // Tuple elements: The owner of the list, the list, the serialized content of the list 
    // The reference to the owner is a WeakReference because you cannot tell the 
    // PropertyDescriptor that you finished the editing and the collection 
    // should be removed from the list. 
    // Remark: The references here may survive the property grid's life 
    private static List<Tuple<WeakReference, IList, byte[]>> collections; 
    private static Timer timer; 

    public ExpandableCollectionPropertyDescriptor(ITypeDescriptorContext context, IList collection, ...) 
    { 
     AddReference(context.Instance, collection); 
     // ... 
    } 

    private static void AddReference(object owner, IList collection) 
    { 
     // TODO: 
     // - serialize the collection into a byte array (BinaryFormatter) and add it to the collections list 
     // - if this is the first element, initialize the timer 
    } 

    private static void Timer_Elapsed(object sender, ElapsedEventArgs e) 
    { 
     // TODO: Cycle through the collections elements 
     // - If WeakReference is not alive, remove the item from the list 
     // - Serialize the list again and compare the result to the last serialized content 
     // - If there a is difference: 
     // - Update the serialized content 
     // - Invoke the CollectionChanged event. The sender is the owner (WeakReference.Target). 
    } 
} 

Теперь вы можете использовать его как это:

public class Form1 : Form 
{ 
    MyObject myObject = new MyObject(); 

    public MyForm() 
    { 
     InitializeComponent(); 
     ExpandableCollectionPropertyDescriptor.CollectionChanged += CollectionChanged(); 
     propertyGrid.SelectedObject = myObject; 
    } 

    private void CollectionChanged(object sender, EventArgs e) 
    { 
     if (sender == myObject) 
      propertyGrid.SelectedObject = myObject; 
    } 
} 

Но если честно, я бы не использовать его вообще. Это имеет серьезные недостатки:

  • Что делать, если элемент коллекции изменен PropertyGrid, но таймер не обновлял последнее внешнее изменение еще?
  • реализатор из IList должен быть сериализуемыми
  • Смешной производительность накладные расходы
  • Хотя с помощью слабых ссылок может уменьшить утечки памяти, это не поможет, если объекты для редактирования имеют более длительный срок службы, чем формы редактора, потому что они будут остаются в статическом наборе
+0

Я не хочу, чтобы он обновлялся одновременно. Я хочу, чтобы это было точно для настройки. Проблема заключается в том, что при расширении списка, а затем его изменении, из 'PropertyGrid' расширенный список не обновляется. Я добавил это и вероятное решение первоначального вопроса –