5

Aunque BindingList<T> y ObservableCollection<T> proporcionar mecanismos para detectar los cambios de la lista, que no son compatibles con los mecanismos para detectar/intercepción cambia antes de que sucedan.Opinión quería: Interceptar cambios en las listas/colecciones

Estoy escribiendo un par de interfaces para apoyar esto, pero quiero expresar su opinión.

Opción 1: Listas provoca eventos para cada tipo de acción

Aquí, los consumidores podrían escribir código como este:

public class Order : Entity 
    { 
     public Order() 
     { 
      this.OrderItems = new List<OrderItem>(); 
      this.OrderItems.InsertingItem += new ListChangingEventHandler<OrderItem>(OrderItems_InsertingItem); 
      this.OrderItems.SettingItem += new ListChangingEventHandler<OrderItem>(OrderItems_SettingItem); 
      this.OrderItems.RemovingItem += new ListChangingEventHandler<OrderItem>(OrderItems_RemovingItem); 
     } 

     virtual public List<OrderItem> OrderItems { get; internal set; } 

     void OrderItems_InsertingItem(object sender, IOperationEventArgs<OrderItem> e) 
     { 
      if (!validationPasses) 
      { 
       e.Cancel = true; 
       return; 
      } 

      e.Item.Parent = this; 
     } 

     void OrderItems_SettingItem(object sender, IOperationEventArgs<OrderItem> e) 
     { 
      if (!validationPasses) 
      { 
       e.Cancel = true; 
       return; 
      } 

      e.Item.Parent = this; 
     } 

     void OrderItems_RemovingItem(object sender, IOperationEventArgs<OrderItem> e) 
     { 
      if (!validationPasses) 
      { 
       e.Cancel = true; 
       return; 
      } 

      e.Item.Parent = null; 
     } 

    } 

Opción 2: Listas plantean un solo evento, y la acción se determina desde el evento args

Aquí, los consumidores pueden escribir un código como este:

public class Order : Entity 
    { 
     public Order() 
     { 
      this.OrderItems = new List<OrderItem>(); 
      this.OrderItems.ListChanging += new ListChangingEventHandler<OrderItem>(OrderItems_ListChanging); 
     } 

     virtual public List<OrderItem> OrderItems { get; internal set; } 

     void OrderItems_ListChanging(object sender, IOperationEventArgs<OrderItem> e) 
     { 
      switch (e.Action) 
      { 
       case ListChangingType.Inserting: 
       case ListChangingType.Setting: 
        if (validationPasses) 
        { 
         e.Item.Parent = this; 
        } 
        else 
        { 
         e.Cancel = true; 
        } 
        break; 

       case ListChangingType.Removing: 
        if (validationPasses) 
        { 
         e.Item.Parent = null; 
        } 
        else 
        { 
         e.Cancel = true; 
        } 
        break; 
      } 
     } 

    } 

Antecedentes: Estoy escribiendo un conjunto de interfaces/clases que representan los componentes básicos de DDD de propósito general, y estoy haciendo el source code available (de ahí la necesidad de crear interfaces amigables).

Esta cuestión se trata de hacer la interfaz como cohesiva como sea posible, para que los consumidores puedan obtener y aplicar sus propias colecciones sin perder la semántica del núcleo.

PD: Por favor, no sugieren el uso de AddXYZ() y RemoveXYZ() métodos para cada lista, porque ya he descarté esa idea.

PPS: I deben incluir el uso de desarrolladores .NET 2.0 :)


Related question.

+0

En el primer caso, sin duda puede usar el mismo controlador para InsertingItem y SettingItem – RichardOD

Respuesta

5

Sugeriría crear algo que sea paralelo al ObservableCollection<T> cuando corresponda. Específicamente, sugeriría seguir las técnicas existentes para notificación de cambio de colección. Algo así como:

class MyObservableCollection<T> 
    : INotifyPropertyChanging, // Already exists 
     INotifyPropertyChanged, // Already exists 
     INotifyCollectionChanging, // You'll have to create this (based on INotifyCollectionChanged) 
     INotifyCollectionChanged // Already exists 
{ } 

Esto le siguen patrones estableció para que los clientes ya están familiarizados con la expuesta interfaces-- tres de las interfaces ya existen. El uso de las interfaces existentes también permitirá una mayor interacción adecuada con otras tecnologías .NET ya existentes, como WPF

Yo esperaría que la interfaz de INotifyCollectionChanged a ser algo así (que se une en contra de las interfaces de INotifyPropertyChanged y INotifyCollectionChanged.):

public interface INotifyCollectionChanged 
{ 
    event CollectionChangingEventHandler CollectionChanging; 
} 

public delegate void CollectionChangingEventHandler(
    object source, 
    CollectionChangingEventArgs e 
); 

/// <remarks> This should parallel CollectionChangedEventArgs. the same 
/// information should be passed to that event. </remarks> 
public class CollectionChangingEventArgs : EventArgs 
{ 
    // appropriate .ctors here 

    public NotifyCollectionChangedAction Action { get; private set; } 

    public IList NewItems { get; private set; } 

    public int NewStartingIndex { get; private set; } 

    public IList OldItems { get; private set; } 

    public int OldStartingIndex { get; private set; } 
} 

Si desea añadir soporte cancelación, simplemente añadir un grabable bool Cancel propiedad a CollectionChangingEventArgs que la colección va a leer para determinar si se ejecuta el cambio que está a punto de ocurrir.

Supongo que esto corresponde a su Opción 2. Este es el camino a seguir porque, para interoperar adecuadamente con otras tecnologías .net que monitorean colecciones cambiantes, tendrá que implementarlo de todos modos para INotifyCollectionChanged. Esto definitivamente seguirá la política de "Least Surprise" en su interfaz.

+0

Gracias Greg. El "Princial of Least Suprise" fue definitivamente el motivo de la opción 2. En mi búsqueda para mantener la compatibilidad con .NET 2.0, tendría que usar el evento ListChanged (INotifyCollectionChanged viene con .NET 3.0). ListChanged expone la enumeración ListChangedType, que no se ajusta exactamente a mis requisitos. Al agregar * más * interfaces/enums a la mezcla, me temo que los desarrolladores estarían alienados por mi biblioteca :( –

2

Recomendaría eventos separados. Me parece más claro.

EDIT:

Es posible que desee cosider un antes y después del evento como insertar, insertado o como los chicos que tienen VB BeforeInsert, AfterInsert. Esto le dará al usuario más flexibilidad.

+0

¿No cree que sería tedioso, tener que agregar todos esos eventhandlers para * cada * lista en la entidad? –

+0

@Vijay: la separación de eventos puede facilitar que los consumidores ignoren explícitamente eventos que no les interesan. ¡Buen trabajo que tienes allí! –

+0

No, esto me permitirá registrar listas a los eventos específicos que requiero, sin un bloque de interruptor genérico. –

2

Tener un vistazo a este link, tal vez eso es lo que está buscando, un objeto basado lista genérica que actúa como una lista, pero con una función de eventos como BeforeItemAdded, ItemAdded, BeforeItemRemoved, ItemRemoved y ItemsCleared.

Espero que esto ayude, Tom. :)

+0

Recomendaría en contra de este enfoque. No maneja adecuadamente situaciones donde un rango de valores cambia, por ejemplo, lo que resulta en una reimplementación de algo que la base ya ha expuesto de una mejor manera. –

2

En realidad, se sorprenderá de lo fácil que puede crear una colección como esa. Eche un vistazo a System.Collections.ObjectModel.Collection<T>. Esa es una clase que está destinada a ser utilizada para tales cosas. Tiene algunos métodos virtuales (uno para cada operación) que puede anular y controlar muy bien.

Recomendaría la opción 1, ya que es más clara y directa.

Este es un ejemplo que se puede utilizar para tal fin:

using System; 
using System.Collections.ObjectModel; 
using System.Collections.Generic; 
using System.Linq; 

namespace TestGround 
{ 
    public class MyCollection<T> : Collection<T> 
    { 
     public class ListChangeEventArgs : EventArgs 
     { 
      public IEnumerable<T> ItemsInvolved { get; set;} 

      public int? Index { get; set;} 
     } 

     public delegate void ListEventHandler(object sender, ListChangeEventArgs e); 

     public event ListEventHandler Inserting; 

     public event ListEventHandler Setting; 

     public event ListEventHandler Clearing; 

     public event ListEventHandler Removing; 

     public MyCollection() : base() { } 

     public MyCollection(IList<T> innerList) : base(innerList) { } 

     protected override void ClearItems() 
     { 
      Clearing(this, new ListChangeEventArgs() 
      { 
       Index = null, 
       ItemsInvolved = this.ToArray(), 
      }); 
      base.ClearItems(); 
     } 

     protected override void InsertItem(int index, T item) 
     { 
      Inserting(this, new ListChangeEventArgs() 
      { 
       Index = index, 
       ItemsInvolved = new T[] { item }, 
      }); 
      base.InsertItem(index, item); 
     } 

     protected override void RemoveItem(int index) 
     { 
      Removing(this, new ListChangeEventArgs() 
      { 
       Index = index, 
       ItemsInvolved = new T[] { this[index] }, 
      }); 
      base.RemoveItem(index); 
     } 

     protected override void SetItem(int index, T item) 
     { 
      Setting(this, new ListChangeEventArgs() 
      { 
       Index = index, 
       ItemsInvolved = new T[] { item }, 
      }); 
      base.SetItem(index, item); 
     } 
    } 
} 

También puede modificar las ListChangeEventArgs tener una propiedad bool con el nombre de "Cancelar", y controlar wheter que hacer el cambio o no en la colección.

Los eventos posteriores también pueden ser útiles, si necesita dicha funcionalidad.

Por supuesto, no tendrá que usar todos los eventos de todas las colecciones, o si es realmente necesario, puede haber otras formas de resolver el problema dependiendo de por qué necesita esta funcionalidad.

EDIT:

Si realmente sólo desea validar los artículos y establecer su propiedad Parent a una instancia de entidad, en realidad se puede escribir una colección que hace exactamente eso, o algo que generaliza el problema de otra manera. Puede pasarle un delegado que valida el artículo, y otro que le dice qué hacer cuando se agrega o elimina un elemento.

Por ejemplo, puede lograrlo utilizando el delegado de Acción.

podría consumir de esta manera:

class Order : Entity 
{ 
    public Order() 
    { 
     OrderItems = new MyCollection2<OrderItem>(
      //Validation action 
      item => item.Name != null && item.Name.Length < 20, 
      //Add action 
      item => item.Parent = this, 
      //Remove action 
      item => item.Parent = null 
     ); 
    } 

    ... 
} 

La principal ventaja de este enfoque es que usted no tiene que molestarse con los controladores de eventos o delegados, por culpa de todo lo que necesita se puede escribir usando las expresiones lambda Sin embargo, si necesita algo más avanzado, siempre puede usar un delegado real en lugar de ellos.

Este es un ejemplo de la colección:

public class MyCollection2<T> : Collection<T> 
{ 
    public Func<T, bool> Validate { get; protected set; } 

    public Action<T> AddAction { get; protected set; } 

    public Action<T> RemoveAction { get; protected set; } 

    public MyCollection2(Func<T, bool> validate, Action<T> add, Action<T> remove) 
     : base() 
    { 
     Validate = Validate; 
     AddAction = add; 
     RemoveAction = remove; 
    } 

    protected override void ClearItems() 
    { 
     foreach (var item in this) 
     { 
      RemoveAction(item); 
     } 
     base.ClearItems(); 
    } 

    protected override void InsertItem(int index, T item) 
    { 
     if (Validate(item)) 
     { 
      AddAction(item); 
      base.InsertItem(index, item); 
     } 
    } 

    protected override void RemoveItem(int index) 
    { 
     RemoveAction(this[index]); 
     base.RemoveItem(index); 
    } 

    protected override void SetItem(int index, T item) 
    { 
     if (Validate(item)) 
     { 
      RemoveAction(this[index]); 
      AddAction(item); 
      base.SetItem(index, item); 
     } 
    } 
} 

Para tales fines, creo que esta es la manera más limpia que ir.

+0

Gracias por la respuesta. De hecho, mi biblioteca ya tiene clases base que implementan la funcionalidad. Esta pregunta se trata de hacer que la interfaz sea lo más cohesiva posible, de modo que los consumidores puedan derivar e implementar sus propias colecciones sin perder la semántica central. –

+0

Edité la publicación para agregar una opción más para ti, sin todos los eventos. Este es un enfoque especializado que le evitaría tener que conectar todos los manejadores de eventos de cada colección en cada entidad. – Venemo

+0

Pero, si desea una solución genérica y no le molesta tener que preocuparse por todos los controladores de eventos, creo que la Opción 1 es la correcta. – Venemo

Cuestiones relacionadas