Story Details for articles

Golf Tracker - Ep. 8 - Code Templates - Part 2 - Views

kahanu
Author:
Version:
Views:
3609
Date Posted:
8/24/2011 7:50:44 PM
Date Updated:
8/24/2011 7:50:44 PM
Rating:
0/0 votes
Framework:
ASP.NET MVC 2
Platform:
Windows
Programming Language:
C#
Technologies:
T4 Templates, T4 Views
Tags:
golf, golf tracker, T4 Views
Demo site:
Home Page:
Share:

Golf Tracker - Code Templates - Part 2 - Views

In the last episode I modified the T4 templates to create a custom controller template.  In this episode I'll start working on modifying the T4 templates for the views.

By modifying the T4 Views templates, you are reducing the amount of manual, repetitive work that would be necessary to generate the views otherwise. And by allowing the tooling to do this work, you are ensuring that you are only including code that you know will work.

Another note about customizing the T4 templates, if you aren't already sold that this is a good idea... another reason that it's extremely helpful to modify these templates is not just to have the tooling write most of the code for you, but take for example that you are using custom CSS styling that needs to be part of each CRUD form.  You can build all of this custom styling into the T4 template modifications.  That by itself is a real time saver!

Preparation

As with the controller modifications, I should have a basis for my views before beginning.  What I like to do is create a fully working set of views and use those as the template for the modifications.  In this case I've fully fleshed out the views for the Course vertical and I'll use those as the basis.

code templates views course folder

And I'll be modifying the default .tt files in the AddView folder that lives inside my application.

code template views

Start Creating Custom Views

Now that I have the working views as a basis and the T4 templates inside my project I can start modifying the view templates.  I'll start with the List.tt to generate the pageable table of Players.

What I'll use as the basis for the List is the Course/Index.aspx view.

01.<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/GolfTrack.Master" Inherits="System.Web.Mvc.ViewPage<GolfTracker.Mvc.Web.ViewData.CourseViewData>" %>
02. 
03.<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
04.    <%= Model.SiteTitle %> - <%= Model.PageTitle %>
05.</asp:Content>
06. 
07.<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
08. 
09.    <h2><%= Model.PageTitle %></h2>
10. 
11.    <p>
12.        <%= Html.ActionLink("Create New", "Create") %>
13.    </p>
14. 
15.     
16.    <% Html.Telerik().Grid(Model.CourseList)
17.           .Name("CourseGrid")
18.           .DataKeys(key => key.Add(c => c.ID))
19.           .Columns(column =>
20.           {
21.               column.Bound(c => c.CourseName);
22.               column.Bound(c => c.Location);
23.               column.Bound(c => c.VoteCount);
24.                
25.               column.Template(action =>
26.                   {%>
27.                        <%= Html.ActionLink("Details", "Details", new{ id= action.ID}) %>
28.                   <%});
29.           })
30.           .Pageable()
31.           .Sortable()
32.           .Render(); %>
33.</asp:Content>

What I intend to do here is to copy this code into the T4 template and set placeholders to replace anything that is specific to a particular model such as Course, or Player, or Tee, etc.  In this case I want to make sure that the CourseViewData class is modified on line 1, the Model.CourseList property on line 16, and the CourseGrid name on line 17.  This is very easy to do.

Here's a look at the default List.tt template that comes with the MVC framework that I'll be modifying.

001.<#@ template language="C#" HostSpecific="True" #>
002.<#@ assembly name="System.Data.Entity" #>
003.<#@ assembly name="System.Data.Linq" #>
004.<#@ import namespace="System.Collections.Generic" #>
005.<#@ import namespace="System.Reflection" #>
006.<#@ import namespace="System.Data.Objects.DataClasses" #>
007.<#@ import namespace="System.Data.Linq.Mapping" #>
008.<#
009.MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
010.string mvcViewDataTypeGenericString = (!String.IsNullOrEmpty(mvcHost.ViewDataTypeName)) ? "<IEnumerable<" + mvcHost.ViewDataTypeName + ">>" : String.Empty;
011.int CPHCounter = 1;
012.bool isFramework4 = (mvcHost.FrameworkVersion >= new System.Version(4, 0));
013.string nugget, htmlEncodeBegin, htmlEncodeEnd;
014.if (isFramework4) {
015.    nugget = ":";
016.    htmlEncodeBegin = "";
017.    htmlEncodeEnd = "";
018.    if (String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
019.        mvcViewDataTypeGenericString = "<dynamic>";
020.    }
021.} else {
022.    nugget = "=";
023.    htmlEncodeBegin = "Html.Encode(";
024.    htmlEncodeEnd = ")";
025.}
026.#>
027.<#
028.// The following chained if-statement outputs the user-control needed for a partial view, or opens the asp:Content tag or html tags used in the case of a master page or regular view page
029.if(mvcHost.IsPartialView) {
030.#>
031.<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<#= mvcViewDataTypeGenericString #>" %>
032. 
033.<#
034.} else if(mvcHost.IsContentPage) {
035.#>
036.<%@ Page Title="" Language="C#" MasterPageFile="<#= mvcHost.MasterPageFile #>" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
037. 
038.<#
039.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
040.        if(cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase)) {
041.#>
042.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
043.    <#= mvcHost.ViewName #>
044.</asp:Content>
045. 
046.<#
047.            CPHCounter++;
048.        }
049.    }
050.#>
051.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= mvcHost.PrimaryContentPlaceHolderID #>" runat="server">
052. 
053.    <h2><#= mvcHost.ViewName #></h2>
054. 
055.<#
056.} else {
057.#>
058.<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
059. 
060.<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
061. 
062.<html xmlns="http://www.w3.org/1999/xhtml" >
063.<head runat="server">
064.    <title><#= mvcHost.ViewName #></title>
065.</head>
066.<body>
067.<#
068.}
069.#>
070.<#
071.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
072.    Dictionary<string, string> properties = new Dictionary<string, string>();
073.    FilterProperties(mvcHost.ViewDataType, properties);
074.#>
075.    <table>
076.        <tr>
077.            <th></th>
078.<#
079.    foreach(KeyValuePair<string, string> property in properties) {
080.#>
081.            <th>
082.                <#= property.Key #>
083.            </th>
084.<#
085.    }
086.#>
087.        </tr>
088. 
089.    <% foreach (var item in Model) { %>
090.     
091.        <tr>
092.<#
093.    List<string> primaryKeys = GetEntityKeyProperties(mvcHost.ViewDataType);
094.    if(primaryKeys.Count > 0) {
095.#>
096.            <td>
097.                <%<#= nugget #> Html.ActionLink("Edit", "Edit", new { id=item.<#= primaryKeys[0] #> }) %> |
098.                <%<#= nugget #> Html.ActionLink("Details", "Details", new { id=item.<#= primaryKeys[0] #> })%> |
099.                <%<#= nugget #> Html.ActionLink("Delete", "Delete", new { id=item.<#= primaryKeys[0] #> })%>
100.            </td>
101.<#
102.    } else {
103.#>
104.            <td>
105.                <%<#= nugget #> Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) %> |
106.                <%<#= nugget #> Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%> |
107.                <%<#= nugget #> Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%>
108.            </td>
109.<#
110.    }
111.     
112.    foreach(KeyValuePair<string, string> property in properties) {
113.#>
114.            <td>
115.                <%<#= nugget #> <#= htmlEncodeBegin #><#= property.Value #><#= htmlEncodeEnd #> %>
116.            </td>
117.<#
118.    }
119.#>
120.        </tr>
121.     
122.    <% } %>
123. 
124.    </table>
125. 
126.    <p>
127.        <%<#= nugget #> Html.ActionLink("Create New", "Create") %>
128.    </p>
129. 
130.<#
131.}
132.#>
133.<#
134.// The following code closes the asp:Content tag used in the case of a master page and the body and html tags in the case of a regular view page
135.#>
136.<#
137.if(mvcHost.IsContentPage) {
138.#>
139.</asp:Content>
140.<#
141.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
142.        if(!cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase) && !cphid.Equals(mvcHost.PrimaryContentPlaceHolderID, StringComparison.OrdinalIgnoreCase)) {
143.            CPHCounter++;
144.#>
145. 
146.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
147.</asp:Content>
148.<#
149.        }
150.    }
151.#>
152.<#
153.} else if(!mvcHost.IsPartialView && !mvcHost.IsContentPage) {
154.#>
155.</body>
156.</html>
157.<#
158.}
159.#>
160. 
161.<#+
162.public void FilterProperties(Type type, Dictionary<string, string> properties) {
163.    if(type != null) {
164.        PropertyInfo[] publicProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
165. 
166.        foreach (PropertyInfo pi in publicProperties)
167.        {
168.            if (pi.GetIndexParameters().Length > 0)
169.            {
170.                continue;
171.            }
172.             
173.            Type currentPropertyType = pi.PropertyType;
174.            Type currentUnderlyingType = System.Nullable.GetUnderlyingType(currentPropertyType);
175.             
176.            if(currentUnderlyingType != null) {
177.                currentPropertyType = currentUnderlyingType;
178.            }
179.             
180.            if (IsBindableType(currentPropertyType) && pi.CanRead)
181.            {              
182.                if(currentPropertyType.Equals(typeof(double)) || currentPropertyType.Equals(typeof(decimal))) {
183.                    properties.Add(pi.Name, "String.Format(\"{0:F}\", item." + pi.Name + ")");
184.                } else if(currentPropertyType.Equals(typeof(DateTime))) {
185.                    properties.Add(pi.Name, "String.Format(\"{0:g}\", item." + pi.Name + ")");
186.                } else {
187.                    properties.Add(pi.Name, "item." + pi.Name);
188.                }
189.            }
190.        }
191.    }
192.}
193. 
194.public bool IsBindableType(Type type)
195.{
196.    bool isBindable = false;
197. 
198.    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)))
199.    {
200.        isBindable = true;
201.    }
202. 
203.    return isBindable;
204.}
205. 
206.public static List<string> GetEntityKeyProperties(Type type)
207.{
208.    List<string> keyProperties = new List<string>();
209. 
210.    PropertyInfo[] properties = type.GetProperties();
211. 
212.    foreach (PropertyInfo pi in properties)
213.    {
214.        System.Object[] attributes = pi.GetCustomAttributes(true);
215. 
216.        foreach (object attribute in attributes)
217.        {
218.            if (attribute is EdmScalarPropertyAttribute)
219.            {
220.                if ((attribute as EdmScalarPropertyAttribute).EntityKeyProperty == true)
221.                {
222.                    keyProperties.Add(pi.Name);
223.                }
224.            } else if(attribute is ColumnAttribute) {
225.                if ((attribute as ColumnAttribute).IsPrimaryKey == true)
226.                {
227.                    keyProperties.Add(pi.Name);
228.                }
229.            }
230.        }
231.    }
232. 
233.    return keyProperties;
234.}
235.#>

A few important things I want to point out that I'll be changing is:
  1. Page directive - on line 10 of the template, the Inherits property of the Page directive contains the generic System.Web.Mvc.ViewPage<> class that takes an IEnumerable<type>.  Since I'm passing a ViewData class that contains both a single model and a list of models to the view, I need to remove the IEnumerable declaration for this to work correctly.
  2. Entity Name - I need a way of getting the name of the model that can be used throughout the template.  So I'll create a little helper method to extract it from the incoming ViewData class.
This is the helper method that extracts the name of the model from the ViewData class so I can make proper replacements where ever I need it in the template.

01.// Parse the incoming Entity type name for the entity name.
02.public static string GetEntityName(string type)
03.{
04.    // Example entity name: ProjectName.Mvc.Web.ViewData.CustomerViewData
05.    // Or:                  ProjectName.BusinessObjects.Customer
06.    string entityName = string.Empty;
07. 
08.    // Get the dot before the Customer segment
09.    int lastDot = type.LastIndexOf('.');
10. 
11.    // Grab that segment to the end of the string, .CustomerViewData or .Customer
12.    string tail = type.Substring(lastDot);
13. 
14.    // Check if this is a ViewData class, if it is we want to chop it off
15.    int viewDataPos = tail.IndexOf("ViewData");
16.    if (viewDataPos > 0)
17.        // This is a ViewData class, so chop it off, returning "Customer"
18.        entityName = tail.Substring(1, viewDataPos - 1);
19.    else
20.        // This is not a ViewData class, so just remove the leading dot, returning "Customer"
21.        entityName = tail.Substring(1);
22. 
23.    return entityName;
24.}

This will be placed at the bottom of the List.tt file and declared at the top like this.  I'll just create a new line after the If statement that ends on line 25 and put this on line 26.

1.string entityName = GetEntityName(mvcHost.ViewDataTypeName);

The mvcHost.ViewDataTypeName will be the string representation of GolfTracker.Mvc.Web.ViewData.CourseViewData which will be the ViewData class passed into the view.  The GetEntityName() method will take this string and set the entityName variable as the word Course.

Once those details are out of the way I can modify the section that renders the table.  So I'll be replacing the following default template code that generates the table...

01.    <table>
02.        <tr>
03.            <th></th>
04.<#
05.    foreach(KeyValuePair<string, string> property in properties) {
06.#>
07.            <th>
08.                <#= property.Key #>
09.            </th>
10.<#
11.    }
12.#>
13.        </tr>
14. 
15.    <% foreach (var item in Model) { %>
16.     
17.        <tr>
18.<#
19.    List<string> primaryKeys = GetEntityKeyProperties(mvcHost.ViewDataType);
20.    if(primaryKeys.Count > 0) {
21.#>
22.            <td>
23.                <%<#= nugget #> Html.ActionLink("Edit", "Edit", new { id=item.<#= primaryKeys[0] #> }) %> |
24.                <%<#= nugget #> Html.ActionLink("Details", "Details", new { id=item.<#= primaryKeys[0] #> })%> |
25.                <%<#= nugget #> Html.ActionLink("Delete", "Delete", new { id=item.<#= primaryKeys[0] #> })%>
26.            </td>
27.<#
28.    } else {
29.#>
30.            <td>
31.                <%<#= nugget #> Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) %> |
32.                <%<#= nugget #> Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%> |
33.                <%<#= nugget #> Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%>
34.            </td>
35.<#
36.    }
37.     
38.    foreach(KeyValuePair<string, string> property in properties) {
39.#>
40.            <td>
41.                <%<#= nugget #> <#= htmlEncodeBegin #><#= property.Value #><#= htmlEncodeEnd #> %>
42.            </td>
43.<#
44.    }
45.#>
46.        </tr>
47.     
48.    <% } %>
49. 
50.    </table>
51. 
52.    <p>
53.        <%<#= nugget #> Html.ActionLink("Create New", "Create") %>
54.    </p>

... with this code that generates the Telerik Grid.

01.<p>
02.    <%= Html.ActionLink("Create New", "Create") %>
03.</p>
04. 
05.<% Html.Telerik().Grid(Model.<#= entityName #>List)
06.       .Name("<#= entityName #>Grid")
07.       .DataKeys(key => key.Add(c => c.ID))
08.       .Columns(column =>
09.       {
10.           column.Template(action =>
11.               {%>
12.                    <%= Html.ActionLink("Edit", "Edit", new{ id = action.ID}) %> |
13.                    <%= Html.ActionLink("Delete", "Delete", new{ id= action.ID}) %>
14.               <%});
15.            
16.            // Enter your specific columns here
17.            
18.           column.Template(action =>
19.               {%>
20.                    <%= Html.ActionLink("Details", "Details", new{ id= action.ID}) %>
21.               <%});
22.       })
23.       .Pageable()
24.       .Sortable()
25.       .Render(); %>

You'll notice that I'm using the entityName variable on lines 5 and 6 to place the correct name of the model into the code.  Also on line 16, I'm simply putting a comment that alerts me to the fact that I need to include the columns that I need for this table.

Since some tables can contain many columns and you may not want to use them all, this just allows me to enter the columns I need and only those.  This isn't such a big deal and I only need to do this once, unless I need to refactor.

At this point the Index.aspx view is done and I can test it.

Modifying the Create and Edit Views

Next I'll modify the create and edit views to work with a convention that I use often which uses a UserControl (partial view) that actually contains the HTML Form.  By using a UserControl I can have the same form used in both the Create and Edit views.  This will only work if these two views generally display the same form fields.  If this is the case, as is in my case, then this is the best way to go so I won't have two separate forms that I need to maintain.  If I need to refactor the form, I make the change in the UserControl and it's reflected in both forms.

Looking at the HTML source for the Create and Edit views looks almost identical.

Create.aspx

01.<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/GolfTrack.Master" Inherits="System.Web.Mvc.ViewPage<GolfTracker.Mvc.Web.ViewData.CourseViewData>" %>
02. 
03.<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
04.    <%= Model.SiteTitle %> - <%= Model.PageTitle %>
05.</asp:Content>
06. 
07.<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
08. 
09.    <h2><%= Model.PageTitle %></h2>
10. 
11.    <% Html.RenderPartial("CourseFormControl", Model.Course); %>
12.</asp:Content>
13. 
14.<asp:Content ID="Content3" ContentPlaceHolderID="HeadContent" runat="server">
15.</asp:Content>

And the Edit.aspx view.

01.<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/GolfTrack.Master" Inherits="System.Web.Mvc.ViewPage<GolfTracker.Mvc.Web.ViewData.CourseViewData>" %>
02. 
03.<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
04.    <%= Model.SiteTitle %> - <%= Model.PageTitle %>
05.</asp:Content>
06. 
07.<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
08. 
09.    <h2><%= Model.PageTitle %></h2>
10. 
11.    <% Html.RenderPartial("CourseFormControl", Model.Course); %>
12.</asp:Content>
13. 
14.<asp:Content ID="Content3" ContentPlaceHolderID="HeadContent" runat="server">
15.</asp:Content>

You'll see on line 11 of both views, is the call to the RenderPartial HTML extension method that calls the CourseFormControl user control.

The CourseFormControl HTML source looks pretty standard, because it is.

01.<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<GolfTracker.BusinessObjects.Course>" %>
02. 
03.    <% using (Html.BeginForm()) {%>
04.        <%= Html.ValidationSummary(true) %>
05.         
06.        <fieldset>
07. 
08.            <div class="editor-label">
09.                <%= Html.LabelFor(model => model.CourseName) %>
10.            </div>
11.            <div class="editor-field">
12.                <%= Html.TextBoxFor(model => model.CourseName, new { @class = "text" })%>
13.                <%= Html.ValidationMessageFor(model => model.CourseName) %>
14.            </div>
15.             
16.            <div class="editor-label">
17.                <%= Html.LabelFor(model => model.Location) %>
18.            </div>
19.            <div class="editor-field">
20.                <%= Html.TextBoxFor(model => model.Location, new { @class = "text" }) %>
21.                <%= Html.ValidationMessageFor(model => model.Location) %>
22.            </div>           
23.             
24.            <div class="editor-label">
25.                <%= Html.LabelFor(model => model.VoteCount) %>
26.            </div>
27.            <div class="editor-field">
28.                <%= Html.TextBoxFor(model => model.VoteCount) %>
29.                <%= Html.ValidationMessageFor(model => model.VoteCount) %>
30.            </div>
31. 
32.            <%= Html.HiddenFor(model => model.rowversion) %>
33.            <p>
34.                <input type="submit" value="Save" />
35.            </p>
36.        </fieldset>
37. 
38.    <% } %>
39. 
40.    <div>
41.        <%= Html.ActionLink("Back to List", "Index") %>
42.    </div>

The only thing added here is the HTMLAttribute on the TextBoxFor for the CSS class otherwise is fairly standard.

I'll first modify the Create.tt file by adding my GetEntityName() helper method into the file and declaring it at the top like I did with the List.tt file.  Here's a look at the default create.tt file.

001.<#@ template language="C#" HostSpecific="True" #>
002.<#@ import namespace="System.Collections.Generic" #>
003.<#@ import namespace="System.Reflection" #>
004.<#
005.MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
006.string mvcViewDataTypeGenericString = (!String.IsNullOrEmpty(mvcHost.ViewDataTypeName)) ? "<" + mvcHost.ViewDataTypeName + ">" : String.Empty;
007.int CPHCounter = 1;
008.bool isFramework4 = (mvcHost.FrameworkVersion >= new System.Version(4, 0));
009.string nugget, htmlEncodeBegin, htmlEncodeEnd;
010.if (isFramework4) {
011.    nugget = ":";
012.    htmlEncodeBegin = "";
013.    htmlEncodeEnd = "";
014.    if (String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
015.        mvcViewDataTypeGenericString = "<dynamic>";
016.    }
017.} else {
018.    nugget = "=";
019.    htmlEncodeBegin = "Html.Encode(";
020.    htmlEncodeEnd = ")";
021.}
022.string entityName = GetEntityName(mvcHost.ViewDataTypeName);
023.#>
024.<#
025.// The following chained if-statement outputs the user-control needed for a partial view, or opens the asp:Content tag or html tags used in the case of a master page or regular view page
026.if(mvcHost.IsPartialView) {
027.#>
028.<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<#= mvcViewDataTypeGenericString #>" %>
029. 
030.<#
031.} else if(mvcHost.IsContentPage) {
032.#>
033.<%@ Page Title="" Language="C#" MasterPageFile="<#= mvcHost.MasterPageFile #>" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
034. 
035.<#
036.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
037.        if(cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase)) {
038.#>
039.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
040.    <#= mvcHost.ViewName #>
041.</asp:Content>
042. 
043.<#
044.            CPHCounter++;
045.        }
046.    }
047.#>
048.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= mvcHost.PrimaryContentPlaceHolderID #>" runat="server">
049. 
050.    <h2><#= mvcHost.ViewName #></h2>
051. 
052.<#
053.} else {
054.#>
055.<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
056. 
057.<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
058. 
059.<html xmlns="http://www.w3.org/1999/xhtml" >
060.<head runat="server">
061.    <title><#= mvcHost.ViewName #></title>
062.</head>
063.<body>
064.<#
065.}
066.#>
067.<#
068.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
069.    Dictionary<string, string> properties = new Dictionary<string, string>();
070.    FilterProperties(mvcHost.ViewDataType, properties);
071.#>
072.    <% using (Html.BeginForm()) {%>
073.        <%<#= nugget #> Html.ValidationSummary(true) %>
074. 
075.        <fieldset>
076.            <legend>Fields</legend>
077.             
078.<#
079.    foreach(KeyValuePair<string, string> property in properties) {
080.#>
081.            <div class="editor-label">
082.                <%<#= nugget #> Html.LabelFor(model => model.<#= property.Key #>) %>
083.            </div>
084.            <div class="editor-field">
085.                <%<#= nugget #> Html.TextBoxFor(model => model.<#= property.Key #>) %>
086.                <%<#= nugget #> Html.ValidationMessageFor(model => model.<#= property.Key #>) %>
087.            </div>
088.             
089.<#
090.    }
091.#>
092.            <p>
093.                <input type="submit" value="Create" />
094.            </p>
095.        </fieldset>
096. 
097.    <% } %>
098. 
099.    <div>
100.        <%<#= nugget #> Html.ActionLink("Back to List", "Index") %>
101.    </div>
102. 
103.<#
104.}
105.#>
106.<#
107.// The following code closes the asp:Content tag used in the case of a master page and the body and html tags in the case of a regular view page
108.#>
109.<#
110.if(mvcHost.IsContentPage) {
111.#>
112.</asp:Content>
113.<#
114.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
115.        if(!cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase) && !cphid.Equals(mvcHost.PrimaryContentPlaceHolderID, StringComparison.OrdinalIgnoreCase)) {
116.            CPHCounter++;
117.#>
118. 
119.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
120.</asp:Content>
121.<#
122.        }
123.    }
124.#>
125.<#
126.} else if(!mvcHost.IsPartialView && !mvcHost.IsContentPage) {
127.#>
128.</body>
129.</html>
130.<#
131.}
132.#>
133. 
134.<#+
135.public void FilterProperties(Type type, Dictionary<string, string> properties) {
136.    if(type != null) {
137.        PropertyInfo[] publicProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
138. 
139.        foreach (PropertyInfo pi in publicProperties)
140.        {
141.            if (pi.GetIndexParameters().Length > 0)
142.            {
143.                continue;
144.            }
145.             
146.            Type currentPropertyType = pi.PropertyType;
147.            Type currentUnderlyingType = System.Nullable.GetUnderlyingType(currentPropertyType);
148.             
149.            if(currentUnderlyingType != null) {
150.                currentPropertyType = currentUnderlyingType;
151.            }
152.             
153.            if (IsBindableType(currentPropertyType) && pi.CanWrite)
154.            {              
155.                if(currentPropertyType.Equals(typeof(double)) || currentPropertyType.Equals(typeof(decimal))) {
156.                    properties.Add(pi.Name, "String.Format(\"{0:F}\", Model." + pi.Name + ")");
157.                } else if(currentPropertyType.Equals(typeof(DateTime))) {
158.                    properties.Add(pi.Name, "String.Format(\"{0:g}\", Model." + pi.Name + ")");
159.                } else {
160.                    properties.Add(pi.Name, "Model." + pi.Name);
161.                }
162.            }
163.        }
164.    }
165.}
166. 
167.public bool IsBindableType(Type type)
168.{
169.    bool isBindable = false;
170. 
171.    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)))
172.    {
173.        isBindable = true;
174.    }
175. 
176.    return isBindable;
177.}
178. 
179. 
180.        // Parse the incoming Entity type name for the entity name.
181.        public static string GetEntityName(string type)
182.        {
183.            // Example entity name: ProjectName.Mvc.Web.ViewData.CustomerViewData
184.            // Or:                  ProjectName.BusinessObjects.Customer
185.            string entityName = string.Empty;
186. 
187.            // Get the dot before the Customer segment
188.            int lastDot = type.LastIndexOf('.');
189. 
190.            // Grab that segment to the end of the string, .CustomerViewData or .Customer
191.            string tail = type.Substring(lastDot);
192. 
193.            // Check if this is a ViewData class, if it is we want to chop it off
194.            int viewDataPos = tail.IndexOf("ViewData");
195.            if (viewDataPos > 0)
196.                // This is a ViewData class, so chop it off, returning "Customer"
197.                entityName = tail.Substring(1, viewDataPos - 1);
198.            else
199.                // This is not a ViewData class, so just remove the leading dot, returning "Customer"
200.                entityName = tail.Substring(1);
201. 
202.            return entityName;
203.        }
204.#>


Then I need to modify the page title content on lines 40 and 50 so it looks like this respectively.

<%= Model.SiteTitle %> - <%= Model.PageTitle %>

... and ...

1.<h2><%= Model.PageTitle %></h2>

Now I just need to remove a chunk of code from lines 67 to 105 which is this code...

01.<#
02.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
03.    Dictionary<string, string> properties = new Dictionary<string, string>();
04.    FilterProperties(mvcHost.ViewDataType, properties);
05.#>
06.    <% using (Html.BeginForm()) {%>
07.        <%<#= nugget #> Html.ValidationSummary(true) %>
08. 
09.        <fieldset>
10.            <legend>Fields</legend>
11.             
12.<#
13.    foreach(KeyValuePair<string, string> property in properties) {
14.#>
15.            <div class="editor-label">
16.                <%<#= nugget #> Html.LabelFor(model => model.<#= property.Key #>) %>
17.            </div>
18.            <div class="editor-field">
19.                <%<#= nugget #> Html.TextBoxFor(model => model.<#= property.Key #>) %>
20.                <%<#= nugget #> Html.ValidationMessageFor(model => model.<#= property.Key #>) %>
21.            </div>
22.             
23.<#
24.    }
25.#>
26.            <p>
27.                <input type="submit" value="Create" />
28.            </p>
29.        </fieldset>
30. 
31.    <% } %>
32. 
33.    <div>
34.        <%<#= nugget #> Html.ActionLink("Back to List", "Index") %>
35.    </div>
36. 
37.<#
38.}
39.#>

... and replace it with this...

1.<% Html.RenderPartial("<#= entityName #>FormControl", Model.<#= entityName #>); %>

The Create.tt file is done!  The simple part is now modifying the edit view like this one and you're done with that view.

Now I'll create the ModelFormControl.tt file that creates the actual HTML form.  The best way to do this is to make a copy of the Edit.tt file and modify that file.  The reason I'm using the Edit.tt as the basis for the ModelFormControl.tt file, is because it already has the code that generates the form with returning values for edit fields, so it works best with both the Create and Edit views.

So in my Visual Studio Solution Explorer, I make a copy of the Edit.tt file and rename it to ModelFormControl.tt.  Now I can open it to modify the code.

This is the ModelFormControl.tt template code before any modifications are made.

001.<#@ template language="C#" HostSpecific="True" #>
002.<#@ import namespace="System.Collections.Generic" #>
003.<#@ import namespace="System.Reflection" #>
004.<#
005.MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
006.string mvcViewDataTypeGenericString = (!String.IsNullOrEmpty(mvcHost.ViewDataTypeName)) ? "<" + mvcHost.ViewDataTypeName + ">" : String.Empty;
007.int CPHCounter = 1;
008.bool isFramework4 = (mvcHost.FrameworkVersion >= new System.Version(4, 0));
009.string nugget, htmlEncodeBegin, htmlEncodeEnd;
010.if (isFramework4) {
011.    nugget = ":";
012.    htmlEncodeBegin = "";
013.    htmlEncodeEnd = "";
014.    if (String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
015.        mvcViewDataTypeGenericString = "<dynamic>";
016.    }
017.} else {
018.    nugget = "=";
019.    htmlEncodeBegin = "Html.Encode(";
020.    htmlEncodeEnd = ")";
021.}
022.#>
023.<#
024.// The following chained if-statement outputs the user-control needed for a partial view, or opens the asp:Content tag or html tags used in the case of a master page or regular view page
025.if(mvcHost.IsPartialView) {
026.#>
027.<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<#= mvcViewDataTypeGenericString #>" %>
028. 
029.<#
030.} else if(mvcHost.IsContentPage) {
031.#>
032.<%@ Page Title="" Language="C#" MasterPageFile="<#= mvcHost.MasterPageFile #>" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
033. 
034.<#
035.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
036.        if(cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase)) {
037.#>
038.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
039.    <#= mvcHost.ViewName #>
040.</asp:Content>
041. 
042.<#
043.            CPHCounter++;
044.        }
045.    }
046.#>
047.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= mvcHost.PrimaryContentPlaceHolderID #>" runat="server">
048. 
049.    <h2><#= mvcHost.ViewName #></h2>
050. 
051.<#
052.} else {
053.#>
054.<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<#= mvcViewDataTypeGenericString #>" %>
055. 
056.<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
057. 
058.<html xmlns="http://www.w3.org/1999/xhtml" >
059.<head runat="server">
060.    <title><#= mvcHost.ViewName #></title>
061.</head>
062.<body>
063.<#
064.}
065.#>
066.<#
067.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
068.    Dictionary<string, string> properties = new Dictionary<string, string>();
069.    FilterProperties(mvcHost.ViewDataType, properties);
070.#>
071.    <% using (Html.BeginForm()) {%>
072.        <%<#= nugget #> Html.ValidationSummary(true) %>
073.         
074.        <fieldset>
075.            <legend>Fields</legend>
076.             
077.<#
078.    foreach(KeyValuePair<string, string> property in properties) {
079.#>
080.            <div class="editor-label">
081.                <%<#= nugget #> Html.LabelFor(model => model.<#= property.Key #>) %>
082.            </div>
083.            <div class="editor-field">
084.                <%<#= nugget #> Html.TextBoxFor(model => model.<#= property.Key #><#= property.Value #>) %>
085.                <%<#= nugget #> Html.ValidationMessageFor(model => model.<#= property.Key #>) %>
086.            </div>
087.             
088.<#
089.    }
090.#>
091.            <p>
092.                <input type="submit" value="Save" />
093.            </p>
094.        </fieldset>
095. 
096.    <% } %>
097. 
098.    <div>
099.        <%<#= nugget #> Html.ActionLink("Back to List", "Index") %>
100.    </div>
101. 
102.<#
103.}
104.#>
105.<#
106.// The following code closes the asp:Content tag used in the case of a master page and the body and html tags in the case of a regular view page
107.#>
108.<#
109.if(mvcHost.IsContentPage) {
110.#>
111.</asp:Content>
112.<#
113.    foreach(string cphid in mvcHost.ContentPlaceHolderIDs) {
114.        if(!cphid.Equals("TitleContent", StringComparison.OrdinalIgnoreCase) && !cphid.Equals(mvcHost.PrimaryContentPlaceHolderID, StringComparison.OrdinalIgnoreCase)) {
115.            CPHCounter++;
116.#>
117. 
118.<asp:Content ID="Content<#= CPHCounter #>" ContentPlaceHolderID="<#= cphid #>" runat="server">
119.</asp:Content>
120.<#
121.        }
122.    }
123.#>
124.<#
125.} else if(!mvcHost.IsPartialView && !mvcHost.IsContentPage) {
126.#>
127.</body>
128.</html>
129.<#
130.}
131.#>
132. 
133.<#+
134.public void FilterProperties(Type type, Dictionary<string, string> properties) {
135.    if(type != null) {
136.        PropertyInfo[] publicProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
137. 
138.        foreach (PropertyInfo pi in publicProperties)
139.        {
140.            if (pi.GetIndexParameters().Length > 0)
141.            {
142.                continue;
143.            }
144.             
145.            Type currentPropertyType = pi.PropertyType;
146.            Type currentUnderlyingType = System.Nullable.GetUnderlyingType(currentPropertyType);
147.             
148.            if(currentUnderlyingType != null) {
149.                currentPropertyType = currentUnderlyingType;
150.            }
151.             
152.            if (IsBindableType(currentPropertyType) && pi.CanRead && pi.CanWrite)
153.            {              
154.                if(currentPropertyType.Equals(typeof(double)) || currentPropertyType.Equals(typeof(decimal))) {
155.                    properties.Add(pi.Name, ", String.Format(\"{0:F}\", Model." + pi.Name + ")");
156.                } else if(currentPropertyType.Equals(typeof(DateTime))) {
157.                    properties.Add(pi.Name, ", String.Format(\"{0:g}\", Model." + pi.Name + ")");
158.                } else {
159.                    properties.Add(pi.Name, String.Empty);
160.                }
161.            }
162.        }
163.    }
164.}
165. 
166.public bool IsBindableType(Type type)
167.{
168.    bool isBindable = false;
169. 
170.    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)))
171.    {
172.        isBindable = true;
173.    }
174. 
175.    return isBindable;
176.}
177.#>

There isn't much to change since it does most of what I want already.  But there are some slight changes I want to make.  I'll be modifying code primarily from lines 66 to 104.  Here's the original code.

01.<#
02.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
03.    Dictionary<string, string> properties = new Dictionary<string, string>();
04.    FilterProperties(mvcHost.ViewDataType, properties);
05.#>
06.    <% using (Html.BeginForm()) {%>
07.        <%<#= nugget #> Html.ValidationSummary(true) %>
08.         
09.        <fieldset>
10.            <legend>Fields</legend>
11.             
12.<#
13.    foreach(KeyValuePair<string, string> property in properties) {
14.#>
15.            <div class="editor-label">
16.                <%<#= nugget #> Html.LabelFor(model => model.<#= property.Key #>) %>
17.            </div>
18.            <div class="editor-field">
19.                <%<#= nugget #> Html.TextBoxFor(model => model.<#= property.Key #><#= property.Value #>) %>
20.                <%<#= nugget #> Html.ValidationMessageFor(model => model.<#= property.Key #>) %>
21.            </div>
22.             
23.<#
24.    }
25.#>
26.            <p>
27.                <input type="submit" value="Save" />
28.            </p>
29.        </fieldset>
30. 
31.    <% } %>
32. 
33.    <div>
34.        <%<#= nugget #> Html.ActionLink("Back to List", "Index") %>
35.    </div>
36. 
37.<#
38.}
39.#>

What I want it to look like is this.

01.<#
02.if(!String.IsNullOrEmpty(mvcViewDataTypeGenericString)) {
03.    Dictionary<string, string> properties = new Dictionary<string, string>();
04.    FilterProperties(mvcHost.ViewDataType, properties);
05.#>
06.    <% using (Html.BeginForm()) {%>
07.        <%<#= nugget #> Html.ValidationSummary(true) %>
08.         
09.        <fieldset>
10.             
11.<#
12.    foreach(KeyValuePair<string, string> property in properties) {
13.#>
14.            <div class="editor-label">
15.                <%<#= nugget #> Html.LabelFor(model => model.<#= property.Key #>) %>
16.            </div>
17.            <div class="editor-field">
18.                <%<#= nugget #> Html.TextBoxFor(model => model.<#= property.Key #><#= property.Value #>) %>
19.                <%<#= nugget #> Html.ValidationMessageFor(model => model.<#= property.Key #>) %>
20.            </div>
21.             
22.<#
23.    }
24.#>
25.            <%= Html.HiddenFor(model => model.rowversion) %>
26.            <p>
27.                <input type="submit" value="Save" />
28.            </p>
29.        </fieldset>
30. 
31.    <% } %>
32. 
33.    <div>
34.        <%<#= nugget #> Html.ActionLink("Back to List", "Index") %>
35.    </div>
36. 
37.<#
38.}
39.#>

You'll see that most of it is the same except I've added the HTML extension method for the Hidden rowversion.  When I'm done and I actually generate the view for this usercontrol, I will most likely have to edit it to remove some unwanted fields, etc.

Once I have these templates modified, I will be able to quickly and easily generate the views.

The only trick is to remember that when I create the Create and Edit views, I need to select the ViewData class as the "View Data" class.  And when I create the ModelFormControl view, I need to select the Business Objects model.  Other than that everything should be good to go.

Now when I want to create a view, I'll see the new ModelFormControl template that I can choose.

code templates view dialog with modelform control

Now creating views will be quick and painless.

I'll go over some more aspects of the code templates in the next episode.

Stay tuned.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)