theKindOfMe

July 1, 2009

Using Custom T4 templates To Generate Better Controllers in ASP MVC

Filed under: Uncategorized — Tags: , , , , — yasi8h @ 7:52 am

While working on this asp.net mvc project at work i couldn’t help notice that there are lots of code that is getting copy-pasted from one controller to the other, when i am adding a new controller. now as you know mvc fm comes with a set of T4 templates that lets you add views and controllers in such a way that some code in these views and controllers are generated for you. this is grate. but you can customize the original templates to your needs and make them generate more of the code for you. (please note that i am talking about controllers that are responsible for mostly CURD on a given single model.)

First of all you will need to identify what code gets repeated from controller to controller in your asp.net mvc project. and then you can get these stuff out in to a template and start working on it.

Locating the T4 Templates

you can find your original templates in [Visual Studio Install Directory]\Common7\IDE\ItemTemplates\[CSharp | VisualBasic]\Web\MVC\CodeTemplates\ and the you can copy them to your project folder. you can start editing these copies and they will be used when you invoke your templates(ex:- by clicking on the Add Controller sub menu within the IDE). for more info please refer to T4 Templates: A Quick-Start Guide for ASP.NET MVC Developers )

Customizing

In my case i wanted my controllers to have all the basic actions like List, Details, Create, Edit and my custom actions like Grid, GridData(take a look at A Grid with Ajax/Pagination/Sorting/Filtering on ASP.net MVC with ExtJS and Enitiy Framework).

i needed to get the type of the model that a given controller is associated with. and then i have to traverse it’s properties(through reflection…etc) and do some string manipulation to generate custom method signatures and custom anonymous types. i also wanted to filter the list of properties that will be used in these parameters and anon types.

Action method signatures

action method signatures can be different from controller to controller. as in…

//Different Properties, in different models
//
//Create action of a Product Controller
public ActionResult Create(string ID, string Name, string Description, string Price)
//
//Create action of a User Controller
public ActionResult Create(string ID, string Name, string Age)

//Different Primary Keys in different models
//
public ActionResult Details(int UID)
//
public ActionResult Details(int UID, int SiteUID)

Method calls to models

//Different Primary Keys
//
public ActionResult Details(int UID)
        {
            return View(Category.GetModelByPrimaryKey(new {UID}));
        }
//
public ActionResult Details(int UID, int SiteUID)
        {
            return View(Category.GetModelByPrimaryKey(new {UID, SiteUID}));
        }

//Different Properties for different models
//
public ActionResult Create(string ID, string Name)
        {
            Category.Create(new {ID,Name}, User.Identity.Name)
            return View();
        }
//
public ActionResult Create(string ID, string Name, int PropOne, string PropTwo)
        {
            Category.Create(new {ID,Name, PropOne, PropTwo}, User.Identity.Name)
            return View();
        }

But unlike in the view here in the controller you can’t just access the model’s type through MvcTextTemplateHost.ViewDataType. when you are generating a controller this property is null. so i thought of using reflection get the model for the current controller. you can use the ControllerRootName property of MvcTextTemplateHost(available through mvcHost variable in the default template) to get the model name(ex: ProductController would give Product).

<#
Type controllerType = Assembly.LoadFile("D:\\path\\to\\your\\model\\assemblys\\dll\\file.dll").GetType("Example.Models." + mvcHost.ControllerRootName);
#>

Following is the full source code of a generated controller and the modified template that was used to generate it. I have reused some methods like IsBindableType and GetEntityKeyProperties(i have modified this a bit) that can be found in some View templates in mvc framework. The generated controller can be used to do CURD operations on the model and provide a backend(providing data to the grid through json) for a ExtJS grid! :) (A Grid with Ajax/Pagination/Sorting/Filtering on ASP.net MVC with ExtJS and Enitiy Framework)

Generated Controller

//
//A Generated Controller
//
using System;
using System.Collections;
using System.Web.Mvc;
using ExampleInc.Filters;
using ExampleInc.Lib.Helpers;
using ExampleInc.Models;
using ExampleInc.Models.UoW;

namespace ExampleInc.Controllers
{
	[Authorize]
    [NavigationFilter]
    public class ExampleModelController : Controller
    {
        //
        // GET: /ExampleModel/
        public ActionResult Index()
        {
            return RedirectToAction("List");
        }

        //
        // GET: /ExampleModel/Details/5
        public ActionResult Details(Int32 SiteUID,Int32 UID)
        {
            return View(ExampleModel.GetModelByPrimaryKey(new {SiteUID,UID}));
        }

        //
        // GET: /ExampleModel/Create
        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /ExampleModel/Create
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(Int32 SiteUID ,Int32 UID ,String ID ,String Name ,Decimal Turnover ,String StreetAddress ,String City ,String PostalCode ,String Telephone1 ,String Telephone2 ,String Email ,String Fax ,String Notes ,String Capacity ,Int16 NumberOfMealsPerDay ,Decimal SellingPricePerMeal ,Decimal BudgetPricePerMeal ,String BestTimeToCall ,Byte TradingDaysPerWeek ,Byte TradingWeeksPerYear ,Boolean AccountActive ,Int32 PrimaryContact_SiteUID ,Int32 PrimaryContactUID ,DateTime LastCallStartTime ,Boolean IsProfileCall )
        {
            if (ViewData.ModelState.IsValid)
            {
                // Attempt to add the user

                if (ExampleModel.Create(new {SiteUID ,UID ,ID ,Name ,Turnover ,StreetAddress ,City ,PostalCode ,Telephone1 ,Telephone2 ,Email ,Fax ,Notes ,Capacity ,NumberOfMealsPerDay ,SellingPricePerMeal ,BudgetPricePerMeal ,BestTimeToCall ,TradingDaysPerWeek ,TradingWeeksPerYear ,AccountActive ,PrimaryContact_SiteUID ,PrimaryContactUID ,LastCallStartTime ,IsProfileCall }, User.Identity.Name))
                {
                    TempData["StatusBar"] += "ExampleModel '" + ID + "' successfully added.";
                    return RedirectToAction("List");
                }

                ModelState.AddModelError("_FORM", "Error");
            }

            // If we got this far, something failed, redisplay form
            return View();
        }

        //
        // GET: /ExampleModel/Edit/5
        public ActionResult Edit(Int32 SiteUID,Int32 UID)
        {
            return View(ExampleModel.GetModelByPrimaryKey(new {SiteUID,UID}));
        }

        //
        // POST: /ExampleModel/Edit/5
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Int32 SiteUID ,Int32 UID ,String ID ,String Name ,Decimal Turnover ,String StreetAddress ,String City ,String PostalCode ,String Telephone1 ,String Telephone2 ,String Email ,String Fax ,String Notes ,String Capacity ,Int16 NumberOfMealsPerDay ,Decimal SellingPricePerMeal ,Decimal BudgetPricePerMeal ,String BestTimeToCall ,Byte TradingDaysPerWeek ,Byte TradingWeeksPerYear ,Boolean AccountActive ,Int32 PrimaryContact_SiteUID ,Int32 PrimaryContactUID ,DateTime LastCallStartTime ,Boolean IsProfileCall )
        {
            if (ViewData.ModelState.IsValid)
            {
                if (ExampleModel.Update(new {SiteUID,UID}, new {SiteUID ,UID ,ID ,Name ,Turnover ,StreetAddress ,City ,PostalCode ,Telephone1 ,Telephone2 ,Email ,Fax ,Notes ,Capacity ,NumberOfMealsPerDay ,SellingPricePerMeal ,BudgetPricePerMeal ,BestTimeToCall ,TradingDaysPerWeek ,TradingWeeksPerYear ,AccountActive ,PrimaryContact_SiteUID ,PrimaryContactUID ,LastCallStartTime ,IsProfileCall }, User.Identity.Name))
                {
                    TempData["StatusBar"] += "ExampleModel '" + ID + "' successfully updated.";
                    return RedirectToAction("List");
                }

                ModelState.AddModelError("_FORM", "Error");
            }

            // If we got this far, something failed, redisplay form
            return View(ExampleModel.GetModelByPrimaryKey(new {SiteUID,UID}));
        }

        public ActionResult Delete(Int32 SiteUID,Int32 UID)
        {
            if (ExampleModel.Delete(new {SiteUID,UID}))
            {
                TempData["StatusBar"] += "ExampleModel successfully deleted.";
            }
            else
            {
                TempData["StatusBar"] += "ExampleModel delete operation failed!.";
            }
            return RedirectToAction("List");
        }

        public ViewResult List()
        {
            return View();
        }

        public ViewResult Grid(string RowClickEventHandlerFunctionName)
        {
            ViewData["RowClickEventHandlerFunctionName"] = RowClickEventHandlerFunctionName;
            return View();
        }

        public JsonResult GridData()
        {
            int totalOjectCount;
            var ExampleModelsList = EmExtJSGridFilterHelper.GetResults(Request, DataMan.ObjectContext.ExampleModel, out totalOjectCount);
            var list = new ArrayList();
            foreach (var ExampleModel in ExampleModelsList) //populate data containers with read data
            {
                list.Add(new
                             {
                                 ExampleModel.SiteUID ,ExampleModel.UID ,ExampleModel.ID ,ExampleModel.Name ,ExampleModel.Turnover ,ExampleModel.StreetAddress ,ExampleModel.City ,ExampleModel.PostalCode ,ExampleModel.Telephone1 ,ExampleModel.Telephone2 ,ExampleModel.Email ,ExampleModel.Fax ,ExampleModel.Notes ,ExampleModel.Capacity ,ExampleModel.NumberOfMealsPerDay ,ExampleModel.SellingPricePerMeal ,ExampleModel.BudgetPricePerMeal ,ExampleModel.BestTimeToCall ,ExampleModel.TradingDaysPerWeek ,ExampleModel.TradingWeeksPerYear ,ExampleModel.AccountActive ,ExampleModel.PrimaryContact_SiteUID ,ExampleModel.PrimaryContactUID ,ExampleModel.LastCallStartTime ,ExampleModel.IsProfileCall
                             });
            }
            return Json(new {dataitExampleInc = list.ToArray(), totalItExampleInc = totalOjectCount});
        }
    }
}

T4 Template used to Generate the Controller

<#@ template language="C#" HostSpecific="True" #>
<#@ output extension="cs" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="System.Data.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Data.Objects.DataClasses" #>
<#@ import namespace="System.Data.Linq.Mapping" #>
<#
MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
#>
<#
Type controllerType = Assembly.LoadFile("D:\\work_items\\ExampleInc\\ExampleInc-Alt-21\\ExampleInc.Models\\bin\\Debug\\ExampleInc.Models.dll").GetType("ExampleInc.Models." + mvcHost.ControllerRootName);

PropertyInfo[] piArray = controllerType.GetProperties(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly);

string actionMethodParamerters = "", anonTypePropValString = "", gridDataAnonTypePropValString = "";
foreach (PropertyInfo pi in piArray)
		{
			Type currentPropertyType = GetUnderlyingType(pi.PropertyType);

			if(IsBindableType(currentPropertyType) && IsNeededProp(pi.Name))
			{
				actionMethodParamerters += currentPropertyType.Name + " " + pi.Name + " ,";
				anonTypePropValString += pi.Name + " ,";
				gridDataAnonTypePropValString += mvcHost.ControllerRootName.ToLower() + "." + pi.Name + " ,";
			}
		}
actionMethodParamerters = actionMethodParamerters.Remove(actionMethodParamerters.Length - 1); //remove the last extra ","
anonTypePropValString = anonTypePropValString.Remove(anonTypePropValString.Length - 1); //remove the last extra ","
gridDataAnonTypePropValString = gridDataAnonTypePropValString.Remove(gridDataAnonTypePropValString.Length - 1); //remove the last extra ","
#>
using System;
using System.Collections;
using System.Web.Mvc;
using ExampleInc.Filters;
using ExampleInc.Lib.Helpers;
using ExampleInc.Models;
using ExampleInc.Models.UoW;

namespace <#= mvcHost.Namespace #>
{
	[Authorize]
    [NavigationFilter]
    public class <#= mvcHost.ControllerName #> : Controller
    {
        //
        // GET: /<#= mvcHost.ControllerRootName #>/
        public ActionResult Index()
        {
            return RedirectToAction("List");
        }

<#
if(mvcHost.AddActionMethods) {
#>
        //
        // GET: /<#= mvcHost.ControllerRootName #>/Details/5
        public ActionResult Details(<#= GetMethodParamertersString(controllerType) #>)
        {
            return View(<#= mvcHost.ControllerRootName #>.GetModelByPrimaryKey(new {<#= GetDataAnonTypePropValString(controllerType) #>}));
        }

        //
        // GET: /<#= mvcHost.ControllerRootName #>/Create
        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /<#= mvcHost.ControllerRootName #>/Create
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(<#= actionMethodParamerters #>)
        {
            if (ViewData.ModelState.IsValid)
            {
                // Attempt to add the user

                if (<#= mvcHost.ControllerRootName #>.Create(new {<#= anonTypePropValString #>}, User.Identity.Name))
                {
                    TempData["StatusBar"] += "<#= mvcHost.ControllerRootName #> '" + ID + "' successfully added.";
                    return RedirectToAction("List");
                }

                ModelState.AddModelError("_FORM", "Error");
            }

            // If we got this far, something failed, redisplay form
            return View();
        }

        //
        // GET: /<#= mvcHost.ControllerRootName #>/Edit/5
        public ActionResult Edit(<#= GetMethodParamertersString(controllerType) #>)
        {
            return View(<#= mvcHost.ControllerRootName #>.GetModelByPrimaryKey(new {<#= GetDataAnonTypePropValString(controllerType) #>}));
        }

        //
        // POST: /<#= mvcHost.ControllerRootName #>/Edit/5
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(<#= actionMethodParamerters #>)
        {
            if (ViewData.ModelState.IsValid)
            {
                if (<#= mvcHost.ControllerRootName #>.Update(new {<#= GetDataAnonTypePropValString(controllerType) #>}, new {<#= anonTypePropValString #>}, User.Identity.Name))
                {
                    TempData["StatusBar"] += "<#= mvcHost.ControllerRootName #> '" + ID + "' successfully updated.";
                    return RedirectToAction("List");
                }

                ModelState.AddModelError("_FORM", "Error");
            }

            // If we got this far, something failed, redisplay form
            return View(<#= mvcHost.ControllerRootName #>.GetModelByPrimaryKey(new {<#= GetDataAnonTypePropValString(controllerType) #>}));
        }

        public ActionResult Delete(<#= GetMethodParamertersString(controllerType) #>)
        {
            if (<#= mvcHost.ControllerRootName #>.Delete(new {<#= GetDataAnonTypePropValString(controllerType) #>}))
            {
                TempData["StatusBar"] += "<#= mvcHost.ControllerRootName #> successfully deleted.";
            }
            else
            {
                TempData["StatusBar"] += "<#= mvcHost.ControllerRootName #> delete operation failed!.";
            }
            return RedirectToAction("List");
        }

        public ViewResult List()
        {
            return View();
        }

        public ViewResult Grid(string RowClickEventHandlerFunctionName)
        {
            ViewData["RowClickEventHandlerFunctionName"] = RowClickEventHandlerFunctionName;
            return View();
        }

        public JsonResult GridData()
        {
            int totalOjectCount;
            var <#= mvcHost.ControllerRootName.ToLower() #>sList = EmExtJSGridFilterHelper.GetResults(Request, DataMan.ObjectContext.<#= mvcHost.ControllerRootName #>, out totalOjectCount);
            var list = new ArrayList();
            foreach (var <#= mvcHost.ControllerRootName.ToLower() #> in <#= mvcHost.ControllerRootName.ToLower() #>sList) //populate data containers with read data
            {
                list.Add(new
                             {
                                 <#= gridDataAnonTypePropValString #>
                             });
            }
            return Json(new {dataitems = list.ToArray(), totalItems = totalOjectCount});
        }
<#
}
#>
    }
}
<#+
public static List<PropertyInfo> GetEntityKeyProperties(Type type)
{
	List<PropertyInfo> keyProperties = new List<PropertyInfo>();

	PropertyInfo[] properties = type.GetProperties();

	foreach (PropertyInfo pi in properties)
	{
		System.Object[] attributes = pi.GetCustomAttributes(true);

		foreach (object attribute in attributes)
		{
			if (attribute is EdmScalarPropertyAttribute)
			{
				if ((attribute as EdmScalarPropertyAttribute).EntityKeyProperty == true)
				{
					keyProperties.Add(pi);
				}
			} else if(attribute is ColumnAttribute) {
				if ((attribute as ColumnAttribute).IsPrimaryKey == true)
				{
					keyProperties.Add(pi);
				}
			}
		}
	}

	return keyProperties;
}

public bool IsBindableType(Type type)
{
	bool isBindable = false;

	if (type.IsPrimitive || type.Equals(typeof(string)) || type.Equals(typeof(DateTime)) || type.Equals(typeof(decimal)) || type.Equals(typeof(Guid)) || type.Equals(typeof(DateTimeOffset)) || type.Equals(typeof(TimeSpan)))
	{
		isBindable = true;
	}

	return isBindable;
}

public bool IsNeededProp(string propName)
        {
            switch (propName)
            {
                case "CreationDate":
                case "LastModifiedDate":
                case "LastModifiedBy":
                case "EffectiveDate":
                case "ExpiryDate":
                case "ServerEntryDate":
                    return false;
            }
            return true;
        }

public string GetMethodParamertersString(Type viewDataType)
        {
			List<PropertyInfo> primaryKeys = GetEntityKeyProperties(viewDataType);

			if(primaryKeys.Count > 0) {
				string result = "";
				foreach(PropertyInfo pk in primaryKeys)
				{
					result += String.Format("{0} {1},", GetUnderlyingType(pk.PropertyType).Name , pk.Name);
				}

				return result.Remove(result.Length - 1); //remove the last extra ","
			} else {
				return "UID=Model.PrimaryKey";
			}
        }

public string GetDataAnonTypePropValString(Type viewDataType)
        {
			List<PropertyInfo> primaryKeys = GetEntityKeyProperties(viewDataType);

			if(primaryKeys.Count > 0) {
				string result = "";
				foreach(PropertyInfo pk in primaryKeys)
				{
					result += String.Format("{0},", pk.Name);
				}

				return result.Remove(result.Length - 1); //remove the last extra ","
			} else {
				return "UID=Model.PrimaryKey";
			}
        }

public Type GetUnderlyingType(Type type)
{
	Type currentPropertyType = type;
			Type currentUnderlyingType = System.Nullable.GetUnderlyingType(currentPropertyType);
			if(currentUnderlyingType != null) {
				currentPropertyType = currentUnderlyingType;
			}

			return currentPropertyType;
}
#>

hope this will help someone/myself in the feuture :)

About these ads

2 Comments »

  1. Dear Friends,

    I hope you are doing well. I have launched a web site http://www.codegain.com and it is basically aimed C#,JAVA,VB.NET,ASP.NET,AJAX,Sql Server,Oracle,WPF,WCF and etc resources, programming help, articles, code snippet, video demonstrations and problems solving support. I would like to invite you as an author and a supporter. Looking forward to hearing from you and hope you will join with us soon.

    Please forward this email to all of your friends who are related IT. Send to us your feed about site also.

    Thank you
    RRaveen
    Founder CodeGain.com

    Comment by RRaveen — July 2, 2009 @ 10:02 am

  2. Hi.. Thanks for the insights. I also found the T4 Toolbox a nice addition to the .NET MVC templates.

    T4 Toolbox: http://www.olegsych.com/2008/08/t4-toolbox/

    Comment by Skelly — September 10, 2009 @ 1:44 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The Silver is the New Black Theme. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: