jueves, 28 de julio de 2016

ASP.NET MVC. ModelBinderProvider para Tipos Genéricos (y II). KeyValuePair Data Binding

Descargar código de ejemplo
Códigos de muestra - Ejemplos MSDN. ModelBinderProvider para tipos genéricos

Artículos anteriores:
ASP.NET MVC. ModelBinderProvider para Tipos Genéricos (I). Creando el escenario

En el artículo anterior vimos cómo MVC no es capaz de realizar de forma automática el enlace de datos para elementos de tipo KeyValuePair.

ASP.NET MVC trata de utilizar el ModelBinder configurado por defecto, el DefaultModelBinder, para realizar el enlace de datos. La forma de actuar con tipos complejos es siempre la misma: intenta crear una instancia del elemento buscando e invocando un constructor sin argumentos y, a continuación, trata de asignar valores a cada una de sus propiedades.

En el caso de KeyValuePair el tipo sí que tiene un constructor sin argumentos por lo que el DefaultModelBinder será capaz de crear la instancia del elemento, sin embargo las propiedades Key y Value son de sólo lectura, por lo que no será capaz de asignar los valores a las propiedades.

En este artículo veremos cómo realizar el enlace de datos para este tipo de datos.

En el artículo ASP.NET MVC. Crear un ModelBinder personalizado. vimos cómo podemos crear un ModelBinder para realizar el enlace de datos de un tipo que ASP.NET MVC no es capaz de enlazar por defecto. La dificultad añadida en este caso es que el tipo de datos es genérico (KeyValuePair<TKey, TValue>). Sin embargo el primer paso que deberemos realizar es el mismo: crear un ModelBinder personalizado.

Creando el ModelBinder para KeyValuePair

Como vimos en el mencionado artículo el ModelBinder deberá implementar la interfaz IModelBinder que consta de un único método BindModel que, a partir de los objetos ControllerContext y ModelBindingContext que recibe como argumentos devuelve el objeto enlazado.

Así que crearé una clase KeyValuePairModelBinder en una nueva carpeta Infrastructure dentro del proyecto, haciendo que esta nueva clase implemente el interfaz IModelBinder:


    public class KeyValuePairModelBinder: IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var values = bindingContext.ValueProvider as ValueProviderCollection;
            if (values == null)
            {
                return null;
            }

            var key = values.GetValue(bindingContext.ModelName + ".Key");
            var keyValue = Convert.ChangeType(key.AttemptedValue, bindingContext.ModelType.GetGenericArguments()[0]);
            var value = values.GetValue(bindingContext.ModelName + ".Value");
            var valueValue = Convert.ChangeType(value.AttemptedValue, bindingContext.ModelType.GetGenericArguments()[1]);
            return Activator.CreateInstance(bindingContext.ModelType, keyValue, valueValue);
        }
    }
Public Class KeyValuePairModelBinder
    Implements IModelBinder

    Public Function BindModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext) As Object _
        Implements IModelBinder.BindModel
        Dim values As ValueProviderCollection = bindingContext.ValueProvider
        If values Is Nothing Then
            Return Nothing
        End If

        Dim key = values.GetValue(bindingContext.ModelName + ".Key")
        Dim keyValue = Convert.ChangeType(Key.AttemptedValue, bindingContext.ModelType.GetGenericArguments()(0))
        Dim value = values.GetValue(bindingContext.ModelName + ".Value")
        Dim valueValue = Convert.ChangeType(value.AttemptedValue, bindingContext.ModelType.GetGenericArguments()(1))
        Return Activator.CreateInstance(bindingContext.ModelType, keyValue, valueValue)
    End Function

End Class

El método recupera a los valores para las propiedades Key y Value a partir del objeto ModelBindingContext y crea una instancia del objeto KeyValuePair utilizando el constructor que reciben los valores de las propiedades Key y Value como argumentos.

Para más detalles sobre cómo crear un ModelBinder para un tipo específico conviene consultar el artículo antes mencionado.

El siguiente paso que seguiríamos en caso de que el tipo de datos no fuese genérico sería el de registrar el ModelBinder añadiéndoselo a la colección Binders de la clase estática ModelBinders. Por desgracia, a la hora de añadir un ModelBinder a esta colección, debemos indicar un tipo específico, no podemos indicar un tipo genérico.

Podríamos añadir una nueva instancia de nuestro ModelBinder por cada implementación específica que utilicemos en el código de nuestro proyecto:

ModelBinders.Binders.Add(typeof(KeyValuePair<string, string>), new KeyValuePairModelBinder()); 
ModelBinders.Binders.Add(typeof(KeyValuePair<string, double>), new KeyValuePairModelBinder());
ModelBinders.Binders.Add(GetType(KeyValuePair(Of String, String)), New KeyValuePairModelBinder()) 
ModelBinders.Binders.Add(GetType(KeyValuePair(Of String, Double)), New KeyValuePairModelBinder())

Evidentemente esta solución es poco práctica y nos obligaría a registrar una nueva instancia del ModelBinder cada vez que queremos utilizar una implementación diferente de KeyValuePair.

Por suerte ASP.NET MVC nos proporciona otro sistema mucho más práctica para poder registrar el ModelBinder: los proveedores de ModelBinders o ModelBinderProviders.



Creando el ModelBinderProvider

Cuando ASP.NET MVC desea realizar el binding de un tipo de datos solicita a los ModelBinderProviders registrados un ModelBinder para dicho tipo. En el momento que un ModelBinderProvider devuelve un ModelBinder lo utiliza para enlazar los datos y deja de buscar.

El ModelBinderProvider configurado por defecto en MVC devolverá el ModelBinder añadido en la colección Binders de la clase ModelBinders para este tipo si existe, si no existe ningún ModelBinder para el tipo especificado devolverá el ModelBinder por defecto: DefaultModelBinder.

Sin embargo podemos crearnos nuestros propios proveedores que devuelvan los ModelBinders para los tipos que nosotros deseemos.

Un proveedor de ModelBinders debe implementar la interfaz IModelBinderProvider, que consta de un único método GetBinder. El método GetBinder recibe como argumento el tipo de datos que MVC trata de enlazar y devuelve un ModelBinder, si el tipo se corresponde con alguno de los soportados por el proveedor, o null si no es capaz de crear un ModelBinder para ese tipo.

La implementación del proveedor será por lo tanto muy sencilla, como podemos ver en el código de la clase KeyValuePairModelBinderProvider que he añadido a la carpeta Infrastructure del proyecto:


    public class KeyValuePairModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(Type modelType)
        {
            if (modelType.IsGenericType &&
                modelType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
            {
                return new KeyValuePairModelBinder();
            }

            return null;
        }
    }
Public Class KeyValuePairModelBinderProvider
    Implements IModelBinderProvider

    Public Function GetBinder(modelType As Type) As IModelBinder Implements IModelBinderProvider.GetBinder
        If modelType.IsGenericType AndAlso _
           modelType.GetGenericTypeDefinition() = GetType(KeyValuePair (Of ,)) Then
            Return New KeyValuePairModelBinder()
        End If

        Return Nothing
    End Function

End Class

Como se puede ver, el método GetBinder comprueba si el tipo recibido es una implementación del tipo genérico KeyValuePair y, si es así, devuelve una nueva instancia de KeyValuePairModelBinder. Si se trata de cualquier otro tipo de datos devuelve null.

Registrando el proveedor

Para indicarle a MVC que incluya nuestro ModelBinderProvider en la lista de proveedores a utilizar cuando trate de realizar el enlace de datos de los argumentos de una acción, deberemos añadir una instancia de nuestro proveedor a la colección BinderProviders de la clase estática ModelBinderProviders.

Así que añadiré una instancia de la clase KeyValuePairModelBinderProvider a la colección BinderProviders en el evento Application_Start del archivo Global.asax:


        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ModelBinderProviders.BinderProviders.Add(new KeyValuePairModelBinderProvider());
        }
    Protected Sub Application_Start()
        AreaRegistration.RegisterAllAreas()
        RouteConfig.RegisterRoutes(RouteTable.Routes)

        ModelBinderProviders.BinderProviders.Add(New KeyValuePairModelBinderProvider())
    End Sub

Y ahora sí, si arrancamos de nuevo la aplicación y rellenamos el formulario:

Formulario con datos
Al pulsar el botón "Validar" para enviar los datos al servidor podremos ver que el enlace de datos se realiza correctamente y los datos se muestran en la vista Result.

Datos correctos


El código completo tanto en C# como en Visual Basic .NET está disponible en:

Códigos de muestra - Ejemplos MSDN. ModelBinderProvider para tipos genéricos

No hay comentarios:

Publicar un comentario