#retosMSDN: Solución al Reto 2 – ¡Esos eventos!

Aquí tienes la solución que proponemos para el segundo de los #retosMSDN: Reto 2 – ¡Esos eventos!. Una vez más, queríamos dar las gracias a todos los que habéis participado en el reto. Vuestra participación nos anima a seguir creando nuevos retos. Y si quieres retar al resto de la comunidad con tu propio reto, recuerda que puedes enviárnoslo a esmsdn@microsoft.com.

 

Nuestra solución

La clave de la resolución de este reto está en entender el propósito del código con la ayuda de los comentarios incluidos, y entender como funcionan los eventos en C#.

 

Según el código y sus comentarios:

1) Al Reto2.EventFired sólo se pueden subscribir métodos de objetos de tipo Item. Los demás son ignorados.

2) Item.Index determina el orden en el que se ejecutarán los métodos de los diferentes objetos Item subscritos.

3) Reto2.FireEvent se utiliza para lanzar el evento.

4) Al lanzar el evento, el método correspondiente (Item.OnEvent en este caso) debería de ser invocado para cada uno de los objetos Item subscritos, en el orden especificado en el punto #2.

Respecto al funcionamiento por defecto de los eventos C#: cuando varios objetos subscriben uno de sus métodos a un evento, al lanzarse el evento dichos métodos serán invocados en el mismo orden en el que se subscribieron.

Así que resumiendo, la idea de este reto es tener un evento en la clase Reto2 al que de igual en qué orden se subscriban los métodos de los objetos Item, ya que al lanzarse el evento dichos métodos se invocarán en el orden especificado por la propiedad Index del Item.

 

Cuando declaramos un evento en nuestra clase podemos utilizar una sintaxis similar a la de las propiedades, e implementar unos métodos add y remove (Event Accessors) que nos permiten modificar el comportamiento cuando algún objeto se subscribe o se de-subscribe. En nuestro caso hemos declarado así el evento:

 

 private EventHandler eventFired;

public event EventHandler EventFired
{
    add
    {
        if ((value != null) && (value.Target is Item))
        {
            this.eventFired += value;

            var orderedDelegates =
                from oneDelegate in this.eventFired.GetInvocationList()
                let item = oneDelegate.Target as Item
                orderby item.Index
                select oneDelegate;

            this.eventFired = (EventHandler)Delegate.Combine(orderedDelegates.ToArray());
        }
    }

    remove
    {
        this.eventFired -= value;
    }
}

 

Lo primero que hacemos en add es comprobar que el objeto del método que se está subscribiendo es de tipo Item, para ignorarlo en caso contrario. Verás que también ignoramos el que alguien se subscriba al evento con un ‘null’, ya que este es el comportamiento por defecto de los eventos en C#. Después subscribimos el método que nos han pasado al evento interno que nos hemos creado en la clase (eventfired). Con LINQ (Language-Integrated Query) ordenamos la lista de métodos subscritos a ese evento interno. Y por último reconstruimos el evento interno ahora ya sí, con la lista de métodos a invocar ordenada tal y como queremos.

La parte de remove es muy sencilla, ya que nos da igual que el método a de-subscribir no sea de un objeto de tipo Item o incluso sea ‘null’; son ignorados sin más por defecto.

 

A la hora de disparar el evento usamos el evento interno, y nos quedaría así de sencillo:

 

 public void FireEvent()
{
    if (this.eventFired != null)
    {
        this.eventFired(this, EventArgs.Empty);
    }
}

Lo más importante de este método es tener en cuenta que si FireEvent fuese llamado antes de que nadie se subscribiese al evento, eventFired estaría a null.

 

El código completo lo puedes encontrar en esta solución de Visual Studio 2013 que puedes descargarte de GitHub.

 

Vuestras soluciones

Como siempre, no hay una única manera de hacer las cosas.

 

@lantoli, por ejemplo, ha ordenado la lista de invocación con LinQ pero utilizando expresiones Lambda, y ha reconstruido la lista de invocación de manera diferente:

 

 private EventHandler handler;

public event EventHandler EventFired
{
    add {
        if (value.Target is Item) {
            var list = handler == null ? new List<EventHandler>() 
                       : handler.GetInvocationList().OfType<EventHandler>().ToList();
            foreach (var evt in list) {
                handler -= evt;
            }
            list.Add(value);
            foreach (var evt in list.OrderBy(x => (x.Target as Item).Index)) {   
                handler += evt;
            }
        }
    }

    remove {
       handler -= value;
    }
}

public void FireEvent() {
    if (handler != null) { 
        handler(this, EventArgs.Empty);
    }
}

 

Por otro lado, a la hora de implementar nuestra solución hemos asumido que normalmente la subscripción de los objetos al evento pasará pocas veces (una vez por objeto), mientras que FireEvent podría ser llamado muchas más veces (aunque en el ejemplo que os proporcionamos sólo se le llame una vez). Por lo que optamos por ordenar la lista de invocación en add. Ahora, en otros casos podría ser más eficiente ordenar la lista en FireEvent. Y esa es la solución por la que habéis optado varios de vosotros, como por ejemplo @javierglozano:

 

 private EventHandler _store;

public event EventHandler EventFired
{
    add
    {
        if (value.Target is Item)
        {
            _store += value;
        }
    }

    remove
    {
        _store -= value;
    }
}

public void FireEvent()
{
    if (_store != null)
    {
        IEnumerable<Delegate> callbacks = from s in _store.GetInvocationList() orderby (s.Target as Item).Index select s;
        foreach (Delegate d in callbacks)
        {
            d.DynamicInvoke(this, EventArgs.Empty);
        }
    }
}

 

¡El próximo viernes 3 de octubre publicaremos el siguiente de nuestros #retosMSDN!

Un saludo,

Alejandro Campos Magencio (@alejacma)

Technical Evangelist

PD: Mantente informado de todas las novedades de Microsoft para los desarrolladores españoles a través del Twitter de MSDN, el Facebook de MSDN, el Blog de MSDN y la Newsletter MSDN Flash.