"If you can't explain it simply, you don't understand it well enough"
- Albert Einstein
As described in my earlier post Creating Proxy-Classes for ViewModel’s (MVVM) by using DynamicObject with DynamicObject’s you have the ability to easily extend and override members of you DomainModel for binding purposes without changing it in any way. You can use that approach in ASP.NET MVC to write easy readable templates, without creating @helper’s for ViewModel related data manipulation.
Let me explain that approach by a simple example. Assuming that we have to build a catalog system based on an existing DomainModel where we have some Articles, n corresponding Prices and due to normalization corresponding price PriceCampaigns to label and specify the Prices.
Let's use the following mocking for our examples:
var listPriceCampaign = new PriceCampaign() { Name = "list" }; var retailPriceCampaign = new PriceCampaign() { Name = "retail" }; var article = new Article() { OrderNumber = "4202.9", Description = "Doomsday Machine" }; var listPrice = new Price() { Value = 99.99m, Article = article, Campaign = listPriceCampaign }; article.Prices.Add(listPrice); listPriceCampaign.Prices.Add(listPrice); var retailPrice = new Price() { Value = 17.99m, Article = article, Campaign = retailPriceCampaign }; article.Prices.Add(retailPrice); retailPriceCampaign.Prices.Add(retailPrice);
We are now able to produce the following output by using our DomainModel:
OrderNumber: 4202.9 Description: Doomsday Machine Price: $17.99 ListPrice: $99.99
To map the prices to the Razor template we can do it either the old fashioned way:
@{ // bad code for determining list- and retail price var listPrice = 0.00m; var retailPrice = 0.00m; foreach(var price in Model.Prices) { if (price.Campaign.Name == "list") listPrice = price.Value; else if (price.Campaign.Name == "retail") retailPrice = price.Value; } } OrderNumber: @Model.OrderNumber Description: @Model.Description Price: $@retailPrice ListPrice: $@listPrice
Or we can do it by using LINQ:
@{ // good code (still bad approach) for determining list- and retail price } OrderNumber: @Model.OrderNumber Description: @Model.Description Price: $@Model.Prices.Where(p => p.Campaign.Name == "retail").First().Value ListPrice: $@Model.Prices.Where(p => p.Campaign.Name == "list").First().Value
Now let’s create the simplified ViewModel-Proxy by using DynamicObject as described in Creating Proxy-Classes for ViewModel’s (MVVM) by using DynamicObject.
public abstract class BaseViewModelProxy<T> : DynamicObject { protected T _domainModel; private PropertyInfo[] _objectProperties; private PropertyInfo[] objectProperties { get { if (objectProperties == null) this._objectProperties = typeof(T).GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); return this._objectProperties; } } public BaseViewModelProxy(T domainModel) { this._domainModel = domainModel; } public override bool TryGetMember(GetMemberBinder binder, out object result) { var pi = this._objectProperties.FirstOrDefault((p) => p.Name == binder.Name); if (pi != null) { result = this._domainModel != null ? pi.GetValue(this._domainModel, null) : null; return true; } else return base.TryGetMember(binder, out result); } public override bool TrySetMember(SetMemberBinder binder, object value) { var pi = this._objectProperties.FirstOrDefault((p) => p.Name == binder.Name); if (pi != null) { if (this._domainModel != null) pi.SetValue(this._domainModel, value, null); return true; } else return base.TrySetMember(binder, value); } }
We also create a BaseCollectionDictionaryModel<T> and BaseModelCollectionDictionaryModel<T, E> that implements IDictionary<string, T> so we are able access collection items by associative indices for collection items in our template.
public abstract class BaseCollectionDictionaryModel<T> : DynamicObject, IDictionary<string, T>, IList<T> { private IList<T> _domainModelCollection; private List<PropertyInfo> _keyFieldPropertyInfoStack; public BaseCollectionDictionaryModel(IList<T> domainModelCollection, string keyFieldPath) { this._domainModelCollection = domainModelCollection; this._keyFieldPropertyInfoStack = new List<PropertyInfo>(); buildPropertyInfoStack(keyFieldPath, typeof(T)); } private void buildPropertyInfoStack(string path, Type type) { var stack = path.Split(new char[] { '.' }); var pi = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == stack[0]); this._keyFieldPropertyInfoStack.Add(pi); if (stack.Length > 1) buildPropertyInfoStack(path.Substring(stack[0].Length + 1), pi.PropertyType); } public T this[string key] { get { // we could also use LINQ Dynamic Expressions instead foreach (T o in this._domainModelCollection) { Object r = o; foreach (PropertyInfo pi in this._keyFieldPropertyInfoStack) r = pi.GetValue(r, null); if ((string)r == key) return o; } throw new KeyNotFoundException(); } set { throw new NotImplementedException(); } } // insert some more IDictionary<string, T>, IList<T> implementations here } public abstract class BaseModelCollectionDictionaryModel<T, E> : BaseCollectionDictionaryModel<E>, IDictionary<string, T>, IList<T> { public BaseModelCollectionDictionaryModel(IList<E> domainModelCollection, string keyFieldPath) : base(domainModelCollection, keyFieldPath) { } public virtual List<T> getModelCollection(IEnumerable<E> items) { throw new NotImplementedException(); } public virtual T getModel(E item) { throw new NotImplementedException(); } public ICollection<string> Keys { get { return base.Keys; } } public ICollection<T> Values { get { return getModelCollection(base.Values); } } public T this[string key] { get { return getModel(base[key]); } set { throw new NotImplementedException(); } } // insert some more IDictionary<string, T>, IList<T> implementations here }
Now to the simple part, the implementation. We've to create some ViewModel classes based on our abstract class BaseViewModelProxy<T>. We also "override" the collection properties so we can use them as IDictionary<string, T> in our template, they will also return our proxified types.
public class ArticleModel : BaseViewModelProxy<Article> { public ArticleModel(Article article) : base(article) { } public PriceModelCollectionDictionaryModel Prices { get { return new PriceModelCollectionDictionaryModel(this._domainModel.Prices); } } } public class PriceModel : BaseViewModelProxy<Price> { public PriceModel(Price price) : base(price) { } public ArticleModel Article { get { return new ArticleModel(this._domainModel.Article); } } public PriceCampaignModel Campaign { get { return new PriceCampaignModel(this._domainModel.Campaign); } } } public class PriceCampaignModel : BaseViewModelProxy<PriceCampaign> { public PriceCampaignModel(PriceCampaign priceCampaign) : base(priceCampaign) { } public PriceModelCollectionDictionaryModel Prices { get { return new PriceModelCollectionDictionaryModel(this._domainModel.Prices); } } } public class PriceModelCollectionDictionaryModel : BaseModelCollectionDictionaryModel<PriceModel, Price> { public PriceModelCollectionDictionaryModel(IList<Price> prices) : base(prices, "Campaign.Name") { } } public class PriceCampaignModelCollectionDictionaryModel : BaseModelCollectionDictionaryModel<PriceCampaignModel, PriceCampaign> { public PriceCampaignModelCollectionDictionaryModel(IList<PriceCampaign> priceCampaigns) : base(priceCampaigns, "Name") { } }
The new template usage is much more simpler and that's how all the hard work pays out:
OrderNumber: @Model.OrderNumber Description: @Model.Description Price: $@Model.Prices["retail"].Value ListPrice: $@Model.Prices["list"].Value
Marc-Anton Flohr