In my last post, I created a partial view page that rendered a collapsible gridview (which I now call a GridTreeView) using the MVCContrib Grid HtmlHelper extension and the jQuery ActsAsTreeTable plug-in. While the code works, there are a few drawbacks. First, I completely forgot about having to link the CSS file in to the view. That’s doable using the view codebehind, but I don’t like codebehinds in MVC. The second drawback is that it doesn’t follow the "standard" method of rendering controls in ASP.NET MVC. All the built-in controls are available via the HtmlHelper object, and that’s how I’d like to expose the GridTreeView as well. Fortunately, it isn’t too terribly difficult to do!
First, let’s define the HtmlHelper extension method (the final version of this, which I will eventually post online, includes various overloaded versions of the method):
1: /// <summary>
2: /// Creates an MvcContrib Grid with the power of the jQuery table-as-tree plug-in.
3: /// </summary>
4: /// <param name="dataSource">The data source to display.</param>
5: /// <param name="attributes">Any HTML attributes to add to the opening table tag.</param>
6: /// <param name="helper">The helper.</param>
7: /// <param name="gridId">The ID to assign to the generated table.</param>
8: /// <param name="idSelector">An expression that selects the ID from an item.</param>
9: /// <param name="parentIdSelector">An expression that selects the parent ID from an item.</param>
10: /// <param name="columns">An expression that generates the columns.</param>
11: /// <param name="sections">An expression that modifies how sections are emitted (such as changing the
12: /// opening tags for rows, columns, etc).</param>
13: public static void GridTreeView<T>(this HtmlHelper helper, string gridId, IEnumerable<T> dataSource,
14: Action<IRootGridColumnBuilder<T>> columns, Func<T, string> idSelector, Func<T, string> parentIdSelector,
15: IDictionary attributes, Action<IGridSections<T>> sections) where T : class
16: {
17: GridColumnBuilder<T> builder = new GridColumnBuilder<T>();
18:
19: if (columns != null)
20: {
21: columns(builder);
22: }
23:
24: if (sections != null)
25: {
26: sections(builder);
27: }
28:
29: GridTreeView<T> grid = new GridTreeView<T>(gridId, dataSource, builder,
30: idSelector, parentIdSelector, attributes,
31: helper.ViewContext.HttpContext.Response.Output,
32: helper);
33:
34: grid.Render();
35: }
Let’s talk through the parameters real quick, because some of them are fairly nasty.
- helper – By prefixing it with the "this" keyword, this method becomes an extension method for HtmlHelper instances, which is what we want.
- gridId – This will be used as the "id" attribute for the table that’s created by the control.
- dataSource – This is any enumerable type. Items from this data source will be used to build the rows of the grid.
- columns – Ok, this one is complicated. It is an Action delegate that takes an IRootGridColumnBuilder. What that means is that the caller must specify a delegate (or lambda) that uses an IRootGridColumnBuilder to define the columns for the grid. This is straight out of the Grid helper from MVCContrib, so go here if you want more info.
- idSelector – This is a delegate (or lambda) that will be called when the grid is being built. The delegate will receive an object from the dataSource as input, and it must return a string representing the ID of the object as output. This is half of what is used to tie parent/child rows together.
- parentIdSelector – This is the other half. This delegate (or lambda) must return the parent ID of the instance that is passed to it.
- attributes – This is a simple key/value pair of attributes to assign to the <table> element of grid.
- sections – You can use this to override how the grid is rendered. For more information, go here.
That’s sounds pretty complicated, but it really isn’t. Here’s how you could use it (I have omitted the markup code for clarity):
1: Html.GridTreeView("MyGridTree", ViewData.Model,
2: column =>
3: {
4: column.For(w => w.Name);
5: column.For(w => w.Description);
6: column.For(w => Html.TextBox("Description_" + w.Id, w.Description), "Editable").DoNotEncode();
7: },
8: w => w.Id.ToString(), w => w.ParentId.ToString(),
9: new Hash(style => "width: 100%"),null
10: );
It may look a little intimidating, but it’s actually quite simple. An IEnumerable containing Widgets is passed in via the ViewData.Model property. Next, you can see the lambda expression that creates columns (one bound to the widget name, one bound to the description, and a text box that is also bound to the description), followed by two lambdas that deal with IDs.
Ok, so I’ve shown you how to define the extension method, and I’ve shown you how to call the method, now I need to show you how to implement the GridTreeView class. Let’s dive into the code:
1: /// <summary>
2: /// An extension of <see cref="Grid{T}"/> that adds
3: /// jQuery ActsAsTree functionality.
4: /// </summary>
5: /// <typeparam name="T">The type of the data item being displayed in the grid.</typeparam>
6: public class GridTreeView<T> : Grid<T> where T : class
7: {
8: #region Const Fields
9:
10: /// <summary>
11: /// The index into the HttpContext.Items bag, it tracks whether or not
12: /// the includes for this control have already been written to the the
13: /// response.
14: /// </summary>
15: private const string mItemKey = "GridViewTree.Initialized";
16:
17: /// <summary>
18: /// The default path to the ActsAsTree javascript file.
19: /// </summary>
20: private const string mDefaultJavaScriptPath = "~/Content/jQueryPlugins/ActsAsTreeTable/jquery.acts_as_tree_table.js";
21:
22: /// <summary>
23: /// The default path to the ActsAsTree CSS file.
24: /// </summary>
25: private const string mDefaultCssPath = "~/Content/jQueryPlugins/ActsAsTreeTable/stylesheets/jquery.acts_as_tree_table.css";
26:
27: #endregion
28:
29: #region Private Delegates
30:
31: /// <summary>
32: /// The function that selects the parent ID from an item.
33: /// </summary>
34: private readonly Func<T, string> GetParent;
35:
36: /// <summary>
37: /// The function that selects the ID from an item.
38: /// </summary>
39: private readonly Func<T, string> GetId;
40:
41: #endregion
42:
43: #region Private Fields
44:
45: /// <summary>
46: /// The DOM ID to assign to the grid.
47: /// </summary>
48: private readonly string mGridId;
49:
50: /// <summary>
51: /// The HTML helper class, which is used to resolve URLs.
52: /// </summary>
53: private readonly HtmlHelper mHelper;
54:
55: #endregion
56:
57: #region Public Static Properties
58:
59: /// <summary>
60: /// The path to the JavaScript ActsAsTree file.
61: /// </summary>
62: /// <remarks>
63: /// This shouldn't need to be changed, but just in case, you can override it by changing this property.
64: /// </remarks>
65: public static string JavaScriptPath { get; set; }
66:
67: /// <summary>
68: /// The path to the CSS file that needs to be included for the tree to display correctly.
69: /// </summary>
70: /// <remarks>
71: /// This shouldn't need to be changed, but just in case, you can override it by changing this property.
72: /// </remarks>
73: public static string CssPath { get; set; }
74:
75: #endregion
76:
77: #region Static Constructors
78:
79: /// <summary>
80: /// Initializes the static fields to default values.
81: /// </summary>
82: static GridTreeView()
83: {
84: JavaScriptPath = mDefaultJavaScriptPath;
85: CssPath = mDefaultCssPath;
86: }
87:
88: #endregion
89:
90: #region Public Constructor
91:
92: /// <summary>
93: /// Creates a new GridTreeView class.
94: /// </summary>
95: /// <param name="dataSource"></param>
96: /// <param name="columnBuilder"></param>
97: /// <param name="htmlAttributes"></param>
98: /// <param name="output"></param>
99: /// <param name="helper"></param>
100: /// <param name="idSelector">A delegate that returns an ID from a T.</param>
101: /// <param name="parentIdSelector">A delegate that returns a parent ID from a T.</param>
102: /// <param name="gridId">The ID to assign to the grid.</param>
103: public GridTreeView(string gridId, IEnumerable<T> dataSource, GridColumnBuilder<T> columnBuilder, Func<T, string> idSelector, Func<T, string> parentIdSelector, IDictionary htmlAttributes, TextWriter output, HtmlHelper helper) : base(dataSource, columnBuilder, htmlAttributes, output, (helper == null ? null : helper.ViewContext.HttpContext))
104: {
105: GetParent = parentIdSelector;
106: GetId = idSelector;
107: mGridId = gridId;
108: mHelper = helper;
109:
110: //Override the ID if it has been set
111: HtmlAttributes["id"] = gridId;
112: }
113:
114: #endregion
115:
116: #region Private Methods
117:
118: /// <summary>
119: /// Renders the row with the default ActsAsTree functionality.
120: /// </summary>
121: /// <param name="item"></param>
122: /// <param name="isAlternate"></param>
123: private void RenderActsAsTreeRow(T item, bool isAlternate)
124: {
125: string row = string.Format("<tr class=\"{0} child-of-node-{1}\" id=\"node-{2}\">",
126: isAlternate ? "gridrow_alternate" : "gridrow",
127: GetParent(item), GetId(item));
128:
129: RenderText(row);
130: }
131:
132: /// <summary>
133: /// Writes the tags to include the required ActsAsTree javascript file.
134: /// </summary>
135: private void WriteJavaScriptInclude()
136: {
137: const string script = @"<script type=""text/javascript"" src=""{0}""></script>";
138:
139: RenderText(string.Format(script, ResolveUrl(JavaScriptPath)));
140: }
141:
142: /// <summary>
143: /// Writes the JavaScript to initialize the grid as an ActsAsTree grid.
144: /// </summary>
145: private void WriteActsAsTreeJavaScript()
146: {
147: const string script =
148: @"<script type=""text/javascript"">
149: $(document).ready(function() {{
150: $(""#{0}"").acts_as_tree_table();
151: }});
152: </script>";
153:
154: RenderText(string.Format(script, mGridId));
155: }
156:
157: /// <summary>
158: /// Writes some HTML to do a dynamic CSS include.
159: /// </summary>
160: private void WriteCssInclude()
161: {
162: const string script =
163: @"<script type='text/javascript'>
164: var link=document.createElement('link');
165: link.setAttribute('rel', 'stylesheet');
166: link.setAttribute('type', 'text/css');
167: link.setAttribute('href', '{0}');
168: var head = document.getElementsByTagName('head')[0];
169: head.appendChild(link);
170: </script>";
171:
172: RenderText(string.Format(script, ResolveUrl(CssPath)));
173: }
174:
175: /// <summary>
176: /// Resolves a URL if an HtmlHelper instance is available, otherwise
177: /// just returns the URL.
178: /// </summary>
179: /// <param name="url"></param>
180: /// <returns></returns>
181: private string ResolveUrl(string url)
182: {
183: return mHelper != null ? mHelper.ResolveUrl(url) : url;
184: }
185:
186: #endregion
187:
188: #region Protected Overrides
189:
190: /// <summary>
191: /// Renders the row.
192: /// </summary>
193: /// <param name="item"></param>
194: /// <param name="isAlternate"></param>
195: protected override void RenderRowStart(T item, bool isAlternate)
196: {
197: //If there's a custom delegate for rendering the start of the row, invoke that instead.
198: if (Columns.RowStartBlock != null)
199: {
200: Columns.RowStartBlock(item);
201: }
202: else if (Columns.RowStartWithAlternateBlock != null)
203: {
204: Columns.RowStartWithAlternateBlock(item, isAlternate);
205: }
206: else
207: {
208: RenderActsAsTreeRow(item, isAlternate);
209: }
210: }
211:
212: #endregion
213:
214: #region Public Methods
215:
216: /// <summary>
217: /// Renders the grid along with all required scripts and resources.
218: /// </summary>
219: public override void Render()
220: {
221: //Include the required CSS/JavaScript if this is the first tree we are rendering.
222: if (Context == null || Context.Items[mItemKey] == null)
223: {
224: //This checks for null first to enable unit testing.
225: if (Context != null) Context.Items[mItemKey] = true;
226:
227: WriteCssInclude();
228: WriteJavaScriptInclude();
229: }
230:
231: //Render the jQuery script to initialize the GridTree.
232: WriteActsAsTreeJavaScript();
233:
234: //Render the GridTree.
235: base.Render();
236: }
237:
238: #endregion
239:
240: }
That’s quite a bit of code, but most of it is straight forward. There are static properties and fields that you can change based on where the jQuery plug-in is installed. The constructor doesn’t do much other than grab references to the parameters so that they can be used later. The neat stuff starts in the overridden methods from Grid.
First, Render checks to see if the necessary JavaScript and CSS files have already been included in the page. If not, they are included using helper methods (more on those in a sec). Next, the JavaScript to turn the grid into a GridTreeView is written. Finally, the actual grid rendering is delegated back to the base class. The only deviation from the standard behavior is in how the row start (tr) tags are written. The RenderRowStart method is overridden, and for the most part, it behaves exactly like the base class. If a caller has chosen to override how rows should be rendered, the specified delegates are called. Otherwise, the helper RenderActsAsTreeRow method is called. This method renders a tr tag with the required class and ID attributes. It does this using the delegates that were passed in to the GridTreeView constructor.
There are a couple of other helper methods that are worth mentioning. First, there is a helper ResolveUrl method. This exists to facilitate unit testing while keeping the code clean. If an HtmlHelper instance was passed to GridTreeView, the call is routed to it, otherwise the method just returns the original URL. Second, the WriteCssInclude method may appear unnecessarily complex upon first inspection. Why does it write JavaScript that creates a link element instead of just emitting the link element? Stylesheet includes are supposed to go in the HTML header of a page. HtmlHelper extensions do not have access to the HTML header, so they cannot add CSS to the page correctly. The JavaScript works around that limitation by inserting the link element into the head even though the JavaScript could potentially be written anywhere in the DOM.
So, that’s basically it. If there is sufficient interest, I’ll wrap everything up in a standalone library (with source code) that can be reused. If you want it, leave me a note in the comments.
want it!
hi there!
thank you very much for sharing the idea. i really appreciate this!
can you please send me the source code of this gridTreeview control
thanks
Krishna
@all
Sorry for the delay in getting this code posted. I need to clean it up a bit first, but I’ve put it on my TODO list to take care of that this weekend. Look for a rare weekend post with the code as well as a pick-up-and-go DLL.
please send me the source code of this gridTreeview control
thanks
For anyone still looking for code, checkout my latest post!