Pass Multiple Models And Anonymous Types To View in ASP.NET MVC
Developers new to ASP.NET MVC often ask these questions:
- How do I pass multiple models to a view?
- How do I pass anonymous objects to a view?
This article provides a solution to both of these questions. Before we
discuss the solution, let's quickly understand these questions and possible
solutions in brief.
How do I pass multiple models to a view?
Consider a case where you have two model classes - Customer and Order.
ASP.NET MVC allows you to pass only ONE model to a view. You typically do that
by passing the model as a View() method parameter. This means a view can be
bound with one and only one model. So, in the preceding example a view can
either display Customer data or Order data. What if a view requires to display
both? You can tackle such a situation as follows:
The first two approaches are quite straightforward and hence I won't discuss
them here. Out of these three the first solution (custom view model class) is a
recommended approach to deal with the situation. However, it calls for creating
of a new view model class (POCO). The second one does the job but the view can't
be strongly typed because models passed through ViewData or ViewBag are not part
of the Model property of the view. The third approach is the topic of this
article and is discussed in detail in the later part of this article.
How do I pass anonymous objects to a view?
Sometimes you need to write LINQ to Entities queries that return data filled
in an anonymous type. Consider the following example:
var query = from c in db.Customers
join o in db.Orders
on c.CustomerID equals o.CustomerID
orderby c.CustomerID ascending
select new {
c.CustomerID,
c.CompanyName,
o.OrderID,
o.OrderDate
};
Here, you are not selecting Customer objects (select c). You are creating an
anonymous type that has four properties - CustomerID, CompanyName, OrderID and
OrderDate. Although this LINQ query works without any problem, the problem is
you can't pass anonymous types to an ASP.NET MVC view. If you try to do so, you
will see a runtime error. Of course, just like previous case you can create a
new view model (POCO with these four properties) and project your data in it.
You can then pass the collection to the View() method. As before this is a
recommended solution but again you need create an additional class. If you wish
to avoid doing that for some reason, here also you can resort to ExpandoObject.
You can store the realized data in an ExpandoObject and then pass it to the
view.
ExpandoObject to the rescue
The ExpandoObject class resides in System.Dynamic namespace and represents an
object whose members can be dynamically added and removed at run time. In our
specific cases an ExpandoObject can act as a container for multiple pieces of
data that you wish to pass as a model to a view. So, in a way ExpandoObject is a
"dynamic" alternative to a manually created view model.
Storing data in an ExpandoObject is straightforward.
dynamic model = new ExpandoObject();
model.Customers = <some_data_here>;
model.Orders = <some_data_here>;
The above code creates an instance of ExpandoObject class. Notice that the
instance is stored in a variable of type dynamic. The code then assigns
Customers and Orders properties of the ExpandoObject to some data. You should be
familiar with this usage because ViewBag uses the same "dynamic" mechanism to
store values.
Ok. Now that you know what an ExpandoObject is and how to use it, let's put
it to use in our specific situations.
Let's assume that you have two model classes - Customers and Orders - as a
part of Entity Framework data model. You wish to pass both of them to a view.
So, you can write something like this in an action method:
public ActionResult Index()
{
NorthwindEntities db=new NorthwindEntities();
dynamic model = new ExpandoObject();
model.Customers = db.Customers.ToList();
model.Orders = db.Orders.ToList();
return View(model);
}
Notice the code marked in bold letters. The code creates an ExpandoObject and
assigns two properties on it - Customers and Orders. These properties store a
generic List of the respective model objects. The ExpandoObject is then passed
to the View() method.
You can use the ExpandoObject in the view as shown below:
@model dynamic
...
<h1>List of Customers</h1>
<div>
@foreach(var item in Model.Customers)
{
<h2>@item.CustomerID</h2>
}
</div>
Notice that the view is marked to use a dynamic model using the @model
dynamic line. This line is optional. If you don't write any @model then Razor
defaults to dynamic. The foreach loop then iterates through Model.Customers
property and displays all the CustomerIDs. Note that since Model points to an
ExpandoObject, you won't get any IntelliSense help for the property names. You
will need to key them in yourself. On the same lines you can display
Model.Orders data.
Store anonymous objects in an ExpandoObject
In the preceding example you stored List of Customer and Order objects
respectively in the model ExpandoObject. Now let's see how to store an anonymous
object in an ExpandoObject.
var addtionalInfo = new {
CustomersNote="This is a customer note",
OrdersNote="This is an order note" };
IDictionary<string, object>
expandoAddInfo = new ExpandoObject();
foreach (PropertyDescriptor property
in
TypeDescriptor.GetProperties(addtionalInfo.GetType()))
{
expandoAddInfo.Add(property.Name,
property.GetValue(addtionalInfo));
}
model.AdditionalInfo = expandoAddInfo;
The code creates an anonymous object with two properties - CustomersNote and
OrdersNote - and stores it in additionalInfo variable. Then an ExpandoObject is
created. This time, however, it is captured as an IDictionary<string, object>.
Then a foreach loop iterates through all the properties of additionalInfo
object. This is done using PropertyDescriptor and TypeDescriptor classes from
System.ComponentModel namespace. These classes allow you to reflect upon the
properties of any object (additionalInfo in this case). All the properties from
the anonymous object are transferred to the ExpandoObject by adding them to the
expandoAddInfo variable. Finally, AdditionalInfo property of model (which itself
is an ExpandoObject) is set to expandoAddInfo ExpandoObject.
You can pass the model object to the view exactly as before. Once received in
the view you can access the AdditionalInfo property as shown below:
<h1>Additional Information</h1>
<h2>@Model.AdditionalInfo.CustomersNote</h2>
<h2>@Model.AdditionalInfo.OrdersNote</h2>
What if you wish to convert a collection of anonymous objects to an
ExpandoObject? The following code shows how this can be done:
var query = from c in db.Customers
join o in db.Orders
on c.CustomerID equals o.CustomerID
orderby c.CustomerID ascending
select new {
c.CustomerID,
c.CompanyName,
o.OrderID,
o.OrderDate
};
List<ExpandoObject> joinData=new List<ExpandoObject>();
foreach(var item in query)
{
IDictionary<string, object> itemExpando = new ExpandoObject();
foreach (PropertyDescriptor property
in
TypeDescriptor.GetProperties(item.GetType()))
{
itemExpando.Add(property.Name, property.GetValue(item));
}
joinData.Add(itemExpando as ExpandoObject);
}
model.JoinData = joinData;
The above code is similar to the previous case but deals with a List of
ExpandoObject (joinData variable). Each ExpandoObject (itemExpando variable)
holds data belonging to a single anonymous object. The joinData List is then
assigned to the JoinData property of the model ExpandoObject.
You can access JoinData inside a view as shown below:
<h1>Customer Orders Join Data</h1>
<div>
@foreach (var item in Model.JoinData)
{
<h2>@item.CustomerID (@item.OrderID on @item.OrderDate)</h2>
}
</div>
A note of caution about using ExpandoObject as shown above - Although
ExpandoObject solves our problem, one should carefully evaluate whether there is
any flaw in the model creation process. You should give a thought to creating POCOs for
holding the multiple pieces instead of using an ExpandoObject. Since
ExpandoObject instances are dynamic Visual Studio IntelliSense won't help much.
POCO containers, on the other hand, give all the benefits of strongly typed
view.
That's it for today! Keep coding!!