I show the technique I use for accessing more than one entity in a 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
{
public class MultiModelBag
{
public bool AllowDuplicates { get; set; }
public List<object> Models { get; set; }
public MultiModelBag (bool allowDupes=false)
{
this.AllowDuplicates = allowDupes;
this.Models = new List<object>();
}
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;
}
public T Get<T>()
{
var found = this.Models.OfType<T>().Select(x=>x);
return (found.Count()>0) ? found.First() : default(T);
}
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;
}
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;
}
public T Remove<T>(string propertyName, object value)
{
PropertyInfo[] properties = typeof(T).GetProperties();
var found = this.Models.OfType<T>().Select(x => x);
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 returns null
.
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
{
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)
{
MultiModelBag bag = new MultiModelBag(true);
bag.Add(new Model1("1"));
bag.Add(new Model2("2a"));
bag.Add(new Model2("2b"));
Model1 bagModel1 = bag.Get<model1>();
Model2 bagModel2 = bag.Get<model2>();
Model2 bagModel2b = bag.Get<model2>("Name", "2b");
Model3 bagModel3 = bag.Get<model3>();
Model2 bagModel2c = bag.Remove<model2>("Name", "2z");
Model2 bagModel2d = bag.Remove<model2>("Name", "2a");
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