Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Multiple Models In a MVC View

0.00/5 (No votes)
5 Nov 2021CPOL5 min read 12.7K   102  
One way to allow access to multiple model entities within a ASP.NET MVC view.
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.

C#
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 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.

C#
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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)