Multiple Models In a MVC View
One way to allow access to multiple model entities within a ASP.NET MVC view.
Introduction
A recent question in the CodeProject QA section regarded the use of multiple models in a given ASP.NET (MVC) view. As you probably know, you can only specify a single model in a view, but there are times when you might need access to more than one at any given time.
My solution is to create a "model bag" that contains a list of objects of type object
, and using that model bag as the one-and-only model in the view, thus adhering to the arbitrary restriction imposed by Microsoft. Keep in mind that this does not mean you can't use this code in other environments, it just explains why the code was originally developed.
This article describes my own model bag implementation, and the code is in a .NET Standard 2.0 assembly, which makes it compatible with both .NET Core (3.0+), and .NET Framework. This is purely to demonstrate the code in both environments. It would be a simple matter to re-target the assembly in question to just target your preferred environment.
Political/religious opinion - Personally, I see no tangible value in using .NET Core - at all - so support for Core in this code is simply an attempt to keep the inevitable triggered whiners at bay. Please don't interpret this staement as an invitation to discuss the subject. I will not be understanding, an will be especially un-kind. You've been warned.
Note to future employers - The political/religious statement above does not mean that I won't work with .NET Core on corporate code. It is just my admittedly narrow view of the world. As an example, I'm not a fan of web development, yet I currently have a job doing it. Such is life.
The Code
The code uses the following .NET features:
- Console applications - for purposes of the demo code
- Library assemblies
- Collections
- Reflection
- Generics
- Linq
Projects
The following projects are included in the demo solution:
- PWMultiModelBag - Class library targeting .NET Standard 2.0. contains the code this article is written to describe
- DemoNetCore - Console app targeting .NET Core 5.0, exercises the PWMultiModelBag assembly.
- DemoNetFrame - Console app targeting .NET Framework 4.7.2, exercises the PWMultiModelBag assembly.
- SampleObjects - Class library targeting .NET Standard 2.0, contains sample classes used to exercise the PWMultiModelBag assembly.
The PWMultiModelBag Assembly
This is the assembly you'll be referencing in your own code. It contains just one class - MultiModelBag
. It's a reasonably simple class with methods for adding objects to its internal collection, retrieving those objects for use by your code, and removing those objects from the internal collection. Here is that class in all of its JSOP goodness.
namespace PWMultiModelBag
{
/// <summary>
/// Provides a model container for ASP.NET views that hods 1 or more models for a given view
/// (this skirts the single-model restriction).
/// </summary>
public class MultiModelBag
{
/// <summary>
/// Get/set a flag indicating whether the programmer can add multiple objects of the
/// same type
/// </summary>
public bool AllowDuplicates { get; set; }
/// <summary>
/// Get/set the collection containing the model objects
/// </summary>
public List<object> Models { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="allowDupes"A flag indicating whether multiple objects of the same type can be added</param>
public MultiModelBag (bool allowDupes=false)
{
this.AllowDuplicates = allowDupes;
this.Models = new List<object>();
}
/// <summary>
/// Adds the specified object to the collection (restricted by AllowDuplicates property)
/// </summary>
/// <param name="obj"></param>
public int Add(object obj)
{
int added = 0;
if (obj != null)
{
var found = this.Models.FirstOrDefault(x=>x.GetType() == obj.GetType());
if (this.AllowDuplicates || found == null)
{
this.Models.Add(obj);
added++;
}
}
return added;
}
/// <summary>
/// Get the first item found in the list that matches the specified type (T).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Get<T>()
{
var found = this.Models.OfType<T>().Select(x=>x);
return (found.Count()>0) ? found.First() : default(T);
}
/// <summary>
/// Get the object in the colllection where the specified propery's value matches.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyName"></param>
/// <param name="value"></param>
/// <returns>The object if found. Otherwise, null.</returns>
/// <remarks>This overload is only useful when dupes are allowed.</remarks>
public T Get<T>(string propertyName, object value)
{
T result = default(T);
PropertyInfo[] properties = typeof(T).GetProperties();
var found = this.Models.OfType<T>();
foreach(object obj in found)
{
PropertyInfo property = properties.FirstOrDefault(x => x.Name == propertyName);
result = (property != null && property.GetValue(obj) == value) ? (T)obj : default(T);
if (result != null)
{
break;
}
}
return result;
}
/// <summary>
/// Removes the 1st occurrence of the specified type from the collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>The object that was removed, or null</returns>
public T Remove<T>()
{
var found = this.Models.OfType<T>().Select(x => x);
T result = (found.Count() > 0) ? found.First() : default(T);
if (result != null)
{
this.Models.Remove(result);
}
return result;
}
/// <summary>
/// Removes the object with the specified value from the bag.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyName"></param>
/// <param name="value"></param>
/// <returns>Returns the removed object</returns>
public T Remove<T>(string propertyName, object value)
{
// gets the properties for the object type
PropertyInfo[] properties = typeof(T).GetProperties();
// find all instances of the specified object type
var found = this.Models.OfType<T>().Select(x => x);
// find the one we want to remove
T result = default(T);
foreach(object obj in found)
{
PropertyInfo property = properties.FirstOrDefault(x => x.Name == propertyName);
result = (property != null && property.GetValue(obj) == value) ? (T)obj : default(T);
if (result != null)
{
this.Models.Remove(result);
break;
}
}
return result;
}
}
}
This object uses generics to allow the programmer to add/get/remove any type of object. It is intended for use with complex objects, as opposed to intrinsics (becauae after all, we are talking about "models" here), but I suppose that intrinsics can be used as well. In the interest of full disclosure, I did not test it with intrinsic objects.
Reflection is used (look for methods that use PropertyInfo
) to find the object with the specified name and value for retrieval or removal.
Instantiation
When you instantiate this object, you can specify that multiple objects of the same type can be added to the bag. When duplicates are allow, there is no protection from adding objects that are identical in terms of property values uswed to identify the objects. The default value for the allowDupes parameter is false
.
Adding Objects
Calling the MultiModelBag.Add
method will perform the following functionality:
if the specified object is not null
{
find the first item of the object's type in the collection
if duplicates are allowed, or an item was not found
{
add the object to the collection
}
}
This method returns 1 (if the object was added), or 0 (if the object was not added).
Removing Objects
Removing objects removes the object of the specified type from the collection, and returns the removed object to the calling method. This is handy because you can a) verify that the correct object was removed, and b) do further processing regardng that object before letting it scope out. Of course, you can also ignore the returned value altogether.
There are two overloads of the MultiModelBag.Remove method.
public T Remove<t>()
- This overload removes the first object it finds of the specified type.
public T Remove<t>(string propertyName, object value)
- This overload accepts a property name and a value, and the collection is searched for the first object of the specified type with the specified property's value, and removes it if found.
Removing objects is not affected by the AllowDuplicates
flag.
Retrieving Objects
Retrieving objects will search for and return the specified object to the calling method. There are two overloads:
public T Get<t>()
- This overload finds and retrieves the first object it finds of the specified type. If an object if the specified type is not found, it returnsnull
.
public T Get<t>(string propertyName, object value)
- This overload accepts a property name and a value, and the collection is searched for the first object of the specified type with the specified property's value, and returns that object (or null if it isn't found).
The SampleObjects Assembly
This assembly provides the sample data used by two consol apps, and is created solely to reduce the demo code footprint. Given the nature of the assembly, no comments were included.
namespace SampleObjects
{
// sample classes used in this demo
public class ModelBase
{
public string Name { get; set; }
}
public class Model1:ModelBase
{
public Model1(string name) { this.Name=name; }
}
public class Model2:ModelBase
{
public Model2(string name) { this.Name=name; }
}
public class Model3:ModelBase
{
public Model3(string name) { this.Name=name; }
}
}
Usage
The two demo console apps are identical in terms of code and functionality, and fully exercise the MultiModelBag
class.
using System;
using PWMultiModelBag;
using SampleObjects;
namespace DemoNetFramework
{
class Program
{
static void Main(string[] args)
{
// the MultiModelBag accepts a bool parameter that indicates whether or
// not to allow multiple objects of the same type.
MultiModelBag bag = new MultiModelBag(true);
bag.Add(new Model1("1"));
bag.Add(new Model2("2a"));
bag.Add(new Model2("2b"));
// should be first model1 obj
Model1 bagModel1 = bag.Get<model1>();
// should be first model2 obj
Model2 bagModel2 = bag.Get<model2>();
// if allowdupes, should be 2nd model2 obj, otherwise will be null
Model2 bagModel2b = bag.Get<model2>("Name", "2b");
// should be null
Model3 bagModel3 = bag.Get<model3>();
// should be null because an object with a "Name" property of "2z" does
// not exist jin the model bag
Model2 bagModel2c = bag.Remove<model2>("Name", "2z");
// should be the same as bagModel2b
Model2 bagModel2d = bag.Remove<model2>("Name", "2a");
// set a breakpoint here to inspect the variables created above
Console.ReadKey();
}
}
}
As you can see, usage is pretty straightforward. We create a few objects and add them to the bag, next we test retrieval, and finally, we test removal. there is nothing special or fancy about this code.
Closing Comments
This code illustrates that it's easy to circumvent some framework extensions (in this example, those imposed within the MVC ecosystem). There may be other (better) ways to achieve the same result, so feel free to search them out and explore them. I can assure you that my feelings won't be hurt.
History
- 2021.11.05 - Initial publication