简体   繁体   中英

ModelBinder for interface for identity of model

I have an IIdentifiable interface:

public interface IIdentifiable
{
    int Id { get; set; }
}

And a simple class Foo :

public class Foo : IIdentifiable
{
    public int Id { get; set; }
    public string Name { get; set; }
}

When I have a page that needs to add a set of Foo and the specify one as the default, I'd have a view model like this:

public class BarViewModel
{
    public IList<Foo> Foos { get; set; }
    public Foo DefaultFoo { get; set; }
}

So in reality I'd like to only pass the Ids around and not the actual full objects inside hidden inputs (that's just nasty and not required). All bar cares about (int the database at least) is between a Bar and it's Foo.Ids and the default Foo.Id.

I was hoping to easily add a model binder that would be able to accept all IIdentifiable s and then set the Id if only an int is set for the value provider. The problem that I ran into is that I can't do something like the following and have it set the Id (since model binders don't look at the derived type chain...ugh):

ModelBinders.Binders[typeof(IIdentifiable)] = new IdentifiableModelBinder();

So I decided to extend the DefaultModelProvider to allow this capability for if the type is an IIdentifiable and the value found in the value provider is just a string/int, then create the model and set the Id property to the matching value:

public class DefaultWithIdentifiableModelBinder : DefaultModelBinder
{

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {

        var modelType = bindingContext.ModelType;
        bool isList = false;

        // Determine the real type of the array or another generic type.
        if (modelType.IsArray)
        {
            modelType = modelType.GetElementType();
            isList = true;
        }
        else if (modelType.IsGenericType)
        {
            var genericType = modelType.GetGenericTypeDefinition();

            if (genericType == typeof(IEnumerable<>) || genericType == typeof(IList<>) || genericType == typeof(ICollection<>))
            {
                modelType = modelType.GetGenericArguments()[0];
                isList = true;
            }
        }

        // The real model type isn't identifiable so use the default binder.
        if (!typeof(IIdentifiable).IsAssignableFrom(modelType))
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        // Get the value provider for the model name.
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        // Get the string values from the value provider.
        var stringValues = valueProviderResult != null ? (IEnumerable<string>)valueProviderResult.RawValue : Enumerable.Empty<string>();

        int tempFirstId = -1;

        // If the first element is an integer, we assume that only the Ids are supplied
        // and therefore we parse the list.
        // Otherwise, use the default binder.
        if (stringValues.Any() && int.TryParse(stringValues.First(), out tempFirstId))
        {

            var listType = typeof(List<>).MakeGenericType(new Type[] { modelType });

            var items = (IList)base.CreateModel(controllerContext, bindingContext, listType);

            // Create each identifiable object and set the Id.
            foreach (var id in stringValues)
            {
                var item = (IIdentifiable)Activator.CreateInstance(modelType);
                item.Id = int.Parse(id);
                items.Add(item);
            }

            if (items.Count == 0)
            {
                return null;
            }

            // Determine the correct result to return.
            if (bindingContext.ModelType.IsArray)
            {
                var array = Array.CreateInstance(modelType, items.Count);
                items.CopyTo(array, 0);
                return array;
            }
            else if (isList)
            {
                return items;
            }
            else
            {
                return items[0];
            }
        }
        else
        {
            return base.BindModel(controllerContext, bindingContext);
        }

    }

}

I'm just not sure if this is really necessary for what I'm trying to do. If anyone could leave feedback/suggestions/improvements on this type of model binding it would be greatly appreciated.

EDIT : Here's a simple example:

An order has many items, so instead of loading the whole object graph of the Item, only the Id is really required for use with the ORM and in the database. Therefore, the item class is loaded with the Id and then the item can be added to the list of items for the order:

public class Order
{
    List<Item> Items { get; set; }
}

var order = new Order();
order.Items.Add(new Item() { Id=2 });
order.Items.Add(new Item() { Id=5 });

This is because the postback to complete the order doesn't send the whole Item , it just sends the Ids.

This is my core requirement. When a postback occurs, I need to build the Items from an Id from the postback. Regardless of if this is a view model or the actual domain model, I still need a way of easily converting from ints to the domain model with the Id set. Make sense?

I have experienced similar problem as following solution.

Firstly, create your interface. It's name is IInterface in following example.

After that, create your CustomBinderProvider like below;

public class IInterfaceBinderProvider : IModelBinderProvider
{
       public IModelBinder GetBinder(ModelBinderProviderContext context)
       {
           if (context.Metadata.ModelType.GetInterface(nameof(IInterface)) != null)
           {
               return new BinderTypeModelBinder(typeof(IInterface));
           }

           return null;
       }
}

Now, you can implement your binder as follows;

public Task BindModelAsync(ModelBindingContext bindingContext)
{
           // Do something

           var model = (IInterface)Activator.CreateInstance(bindingContext.ModelType);

           // Do something

           bindingContext.Model = model;
           bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
           return Task.FromResult(model);
} 

Finally, you have to insert your provider to Startup class as follows;

services.AddMvcCore(x =>
{
     x.ModelBinderProviders.Insert(0, new IInterfaceBinderProvider());
})

This seems like a decent way but not very extensible. If you're unlikely to have this same situation arise with different interfaces, then it's a fine solution.

An alternative would be to use reflection at startup to find all types that implement IIdentifiable and assign the custom model binder for all of them.

Well, lets suppose you need Foo and its data somewhere. Therefore you save the Name for it (maybe along with other properties) and retrieve all that data from persistence as a Foo instance and this is exactly what your domain entity for . It contains all the data related to Foo .

On the other side you have a View where the only thing you want operate with is Id so you create the ViewModel which contains the Id as property (and maybe more Ids like ParentFooId for example) and this is what your ViewModel for . It contains only data specific to your View - its like an interface between your View and Controller.

That way everything is done with DefaultModelBinder . For example, if you have:

  • an id (of type int ) parameter in your RouteData dictionary or have it posted via form;
  • BarViewModel instance as a parameter of your controller's Action;
  • Id property of BarViewModel

then on request the value of that barViewModel.Id property will be the value from your RouteData (or your form) because DefaultModelBinder is capable of that. And you only create custom IModelBinder for really unusual scenario.

I just don't see a reason to overcomplicate things.

Makes sense?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM