2015-09-15 14 views
9

Desidero mostrare automaticamente ogni IList come espandibile nel mio PropertyGrid (Per "espandibile", ovviamente intendo che gli elementi verranno visualizzati). Non voglio utilizzare gli attributi di ciascuna lista (ancora una volta, voglio che funzionare per ogni IList)Raccolta espandibile PropertyGrid

ho cercato di achive esso utilizzando una consuetudine PropertyDescriptor ed un ExpandableObjectConverter. Funziona, ma dopo aver eliminato gli elementi dall'elenco, lo PropertyGrid non viene aggiornato, continuando a visualizzare gli elementi eliminati.

Ho provato a utilizzare ObservableCollection insieme ad aumentare OnComponentChanged e anche attributo RefreshProperties, ma non ha funzionato.

Questo è il mio codice:

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

e lo uso per tutti IList s con la seguente riga:

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

alcuni chiarimenti

voglio griglia per aggiornare automaticamente quando cambio la lista. L'aggiornamento quando un'altra proprietà cambia, non aiuta.

una soluzione che funziona, è una soluzione in cui:

  1. Se si espande la lista mentre è vuota, e quindi aggiungere gli elementi, la griglia viene aggiornata con gli articoli espansi
  2. Se si aggiungono elementi alla lista, espanderla e quindi rimuovere gli elementi (senza collassare), la griglia viene aggiornata con gli elementi espansi e non lanciando ArgumentOutOfRangeException perché sta tentando di mostrare elementi che sono stati già eliminati
  3. Voglio tutto questo per un utilità di configurazione. Solo il PropertyGrid dovrebbe cambiare le collezioni

EDIT IMPORTANTE:

sono riuscito a rendere le collezioni espansi aggiornano con Reflection, e chiamando NotifyValueGivenParent metodo sull'oggetto context quando il metodo GetValue PropertyDescriptor viene chiamato (quando RefreshRequired evento viene generato):

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

funziona perfettamente, tranne che provoca l'evento essere innalzato infinite volte, perché chiamare NotifyValueGivenParent causa un ricaricamento di PropertyDescriptor e, quindi, innalzare l'evento e così via.

ho cercato di risolverlo aggiungendo una semplice bandiera che impedirà la ricarica se è già ricaricando, ma per qualche ragione NotifyValueGivenParent si comporta in modo asincrono, e quindi la ricarica avviene dopo la bandiera è spento. Forse è un'altra direzione da esplorare.L'unico problema è la ricorsione

+0

Perché non si chiama 'TypeDescriptor.AddAttributes (typeof (IList), nuovo TypeConverterAttribute (typeof (ExpandableObjectConverter)))?' invece della classe personalizzata? –

+0

@SimonMourier perché non riesco a vedere gli elementi nella raccolta, ma le proprietà 'Capacity' e' Count' –

+0

Questo requisito non viene visualizzato nella domanda. A proposito, funziona per me con una proprietà di tipo ArrayList. Suppongo che dipenda dalla classe in SelectedObject. dovresti rispondere alla tua domanda con tutto il codice pertinente e l'intera domanda. –

risposta

2

Non è necessario utilizzare ObservableCollection. È possibile modificare la classe descrittore come segue:

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

Invece del ExpandableCollectionConverter vorrei derivare la classe CollectionConverter, in modo da poter ancora utilizzare il pulsante puntini di sospensione per modificare l'insieme alla vecchia maniera (in modo da poter aggiungere/rimuovere elementi se la raccolta non è di sola lettura):

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

e vorrei utilizzare questo ListConverter sulle proprietà dove voglio vedere elenco espandibile. Ovviamente, è possibile registrare il convertitore di tipi in genere come nel tuo esempio, ma questo sovrascrive tutto, il che potrebbe non essere nel complesso previsto.

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

Importante: L'attributo RefreshProperties dovrebbe essere definito per le proprietà che modificano altre proprietà. In questo esempio, la modifica di Count sostituisce l'intero elenco.

Usandolo come propertyGrid1.SelectedObject = new MyClass(); produce il seguente risultato:

enter image description here

+2

Questo non è quello che volevo. Non voglio che si aggiorni quando si aggiorna un'altra proprietà. Voglio che si aggiorni quando la lista è cambiata. Aggiungo articoli alla lista, espandi, aggiungi altri elementi, ma gli articoli non sono aggiornati –

+0

Grazie! Grande esempio - esattamente quello di cui avevo bisogno. – Glenn

3

Non voglio che per rinfrescare quando altri rinfresca proprietà. Voglio che si aggiorni quando la lista è cambiata. Aggiungo elementi all'elenco, espanderla, aggiungere altri elementi, ma gli elementi non vengono aggiornati

Questo è un abuso tipico delle PropertyGrid. È per la configurazione di un componente e non per riflettere le modifiche simultanee al volo da una fonte esterna. Anche il wrapping del IList in un ObservableCollection non ti aiuterà perché è utilizzato solo dal tuo descrittore, mentre l'origine esterna manipola direttamente l'istanza IList sottostante.

Che cosa si può ancora fare è un particolarmente brutto hack:

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). 
    } 
} 

Ora è possibile utilizzare in questo modo:

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

Ma onestamente, io non lo uso affatto. Essa ha gravi difetti:

  • Che cosa succede se un elemento collezione è cambiata dal PropertyGrid, ma il timer non ha ancora aggiornato l'ultima modifica esterna?
  • Il realizzatore del IList deve essere serializzabile
  • prestazioni Ridicolo sovraccarico
  • Anche se utilizzando riferimenti deboli può ridurre le perdite di memoria, non aiuterà se gli oggetti per modificare avere ciclo di vita più lungo del modulo Editor, perché saranno rimanere nella collezione statica
+0

Non voglio che si aggiorni contemporaneamente. Lo voglio esattamente per la configurazione. Il problema è che quando si espande un elenco e quindi lo si cambia, da "PropertyGrid" l'elenco espanso non viene aggiornato. Ho aggiunto questo e una probabile soluzione alla domanda originale –