I'm trying to write an auto-scaffolder template for Index views. I'd like to be able to pass in a collection of models or view-models (e.g., IQueryable<MyViewModel>) and get back an HTML table that uses the DisplayName attribute for the headings (th elements) and Html.Display(propertyName) for the cells (td elements). Each row should correspond to one item in the collection.
Here's what I have so far:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%
var items = (IQueryable<TestProj.ViewModels.TestViewModel>)Model;
// How do I make this generic?
var properties = items.First().GetMetadata().Properties
.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm));
%>
<table>
<tr>
<%
foreach(var property in properties)
{
%>
<th>
<%= property.DisplayName %>
</th>
<%
}
%>
</tr>
<%
foreach(var item in items)
{
HtmlHelper itemHtml = ????;
// What should I put in place of "????"?
%>
<tr>
<%
foreach(var property in properties)
{
%>
<td>
<%= itemHtml.Display(property.DisplayName) %>
</td>
<%
}
%>
</tr>
<%
}
%>
</table>
Two problems with this:
I'd like it to be generic. So, I'd like to replace var items = (IQueryable<TestProj.ViewModels.TestViewModel>)Model; with var items = (IQueryable<T>)Model; or something to that effect.
A property Html is automatically created for me when the view is created, but this HtmlHelper applies to the whole collection. I need to somehow create an itemHtml object that applies just to the current item in the foreach loop. I'm not sure how to do this, however, because the constructors for HtmlHelper don't take a Model object.
How do I solve these two problems?
Phil Haack to the rescue!
http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx
for the geniric IQueryable part, why don't you simply cast it to an IQueryable...
var items = (IQueryable<TestProj.ViewModels.TestViewModel>)Model;
and What is your template for, isn't it almost the same as the default list template when you add a view... If you want to customize it, i think you'd better look for the List.tt template. You could modify that to use Html.Display instead of Html.Encode.
Or you could try using
Html.DisplayFor(o => property.GetValue(item))
I'm not sure if this would work...
Sadly you can't do:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<List<T>>" %>
or even
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<T>" %>
ASP.NET MVC implements a Html.DisplayForModel() as Brad Wilson shows on his blog which does scaffolding for you out of the box. (which I'm sure you know about, mentioning for completeness).
If you look at the MVC 2 RTM Source Code source code and in particular the DefaultDisplayTemplates.cs it's all basically contained in an HTMLHelper as suppose to a View. Perhaps instead of fighting with the auto generated class for the view (as much as I love asp.net mvc the default view rendering engine is a total pain) go with an HTMLHelper extension modelled after how ASP.NET MVC RTM implements scaffolding. I guess this would open the possibility for code reuse some more too (though almost everything is internal).
It would be nice if the ASP.NET MVC scaffolding would allow for a custom template to be passed in to change the html if you're not a fan of the default <div class=\"display-field\"> etcetera. To me this is one of the weak spots of ASP.net MVC (the 'hardcoded' error handling/ scaffolding HTML).
If your not a fan of string building your HTML, your extension could call out to more granular partial views (like ScaffoldCaption.ascx) when your scaffolding HTMLHelper extension has gathered enough information to sufficiently type the granular view.
Have you looked into the MVCContrib Grid? Certainly someone has written an extension method to show all properties by default. If not it shouldn't be difficult.
edit2- I guess don't trust my answers on Sunday. I misread the question before and didn't realize Html.Display() was the MVC2 templated input. (I'm doing some radically different input builders at my shop so I didn't pick up on the name. I just thought Display showed the value.) Anyway, there are two options I see here.
1) Pull the MVC2 source or break out Reflector and write your own method that is not extending HtmlHelper.
2) You can use reflection here to build a new HtmlHelper instance but it's not pretty. (I didn't test this but is should work.)
var modelType = Html.GetType().GetGenericParameters()[0];
var itemHtmlType = typeof(HtmlHelper<>).MakeGenericType(modelType);
var itemHtmlCtor = itemHtmlType.GetConstructor(typeof(ViewContext), typeof(IViewDatacontainer), typeof(RouteCollection));
var itemHtml = itemHtmlCtor.Invoke(Html.ViewContext, Html.ViewDataContainer, Html.RouteCollection);
You'll get an object out of this though so you have to use reflection to invoke itemHtml.Dipslay. I'd recommend going a different route.
Related
I am currently working on converting an existing webforms application to MVC and am not sure how to design one of the pages. The existing page is 2500 lines of code, and is responsible for displaying several different views of the same information. The following example is just a small piece of this page (a toolbar), but I'm not sure what the best approach is to avoid duplicating code.
Administrator view toolbar:
Save, PDF, Print, Spell Check, Administrative Documents, Employee View Toggle
Manager view of toolbar:
Save, PDF, Print, Spell Check, Employee View Toggle
Employee view of toolbar:
Save, PDF, Print, Spell Check
I have split the application into three different views. They use the same partial views to render the content of the page, but currently each view has it's own copy of the toolbar. So the HTML elements for Save, PDF, Print and Spell Check are duplicated in each of the views. It would be ideal to have a partial view to render the toolbar, but in order to do that I would need to put some sort of logic in the view to determine if it should show the Employee View Toggle link or the Administrative Documents.
I have also ran into similar problems while developing other new pages. Initially the requirements for different pages have the same element on them that we split into a partial view. After testing and adding more functionality, eventually the partial view needs to be slightly different on each of the pages. Usually it's just the visibility of the certain controls, but eventually the partial views end up having a bunch of logic in them to check several things to determine if something should be shown.
What is the best approach for dealing with slightly different views where most of the elements are the same?
If it's just an issue of permissions showing or hiding different parts of the same control/partial view/whatever, I would say just write one and have some code in it to check permissions and conditionally render or don't render various parts. Something like this:
<%# Control Language="C#" %>
This is the toolbar!!
<div class="fake-css-class">
<%:Html.ActionLink("Save", "Save") %>
<%:Html.ActionLink("PDF", "Pdf") %>
<%:Html.ActionLink("Spell Check", "SpellCheck") %>
<% if (CurrentUser.IsInRole("Admin") { %>
<%: Html.ActionLink("Administrative Documents", "AdminDocs") %>
<%} %>
<% if (CurrentUser.IsInRole("Admin") || CurrentUser.IsInRole("Manager"){
%>
<%: Html.ActionLink("Employee View Toggle", "EmpView") %>
<%} %>
</div>
<br />
<br />
EDIT:
When the logic gets more complicated, add functions to your security code so that you can separate all the logic out into your model/controller and in the view you're making a single function call. For example something like this:
public bool Authorize(string controllerName, string actionName) {
bool authorize = false;
foreach(var permission in this.permissions) {
if (permission.Matches(controllerName, actionName)) {
authorize = permission.Affirmative;
}
}
return authorize;
}
This way you could have the code outside of the view and simply a single line of code in the view. The above code is just taken out of my current project and used as an example, but you can write whatever complicated logic and put it out in the model or controller so your view stays clean and you aren't putting business logic in there.
I am using ASP.NET MVC 2 & .Net 3.5 with Visual Studio 2008.
Ok, what I am referring to by 'Wizard type page navigation', is a site where you have a list of stages in a given process or workflow. There is some kind of visual denotation to indicate which part of the stage you are at. I have already implemented this part (albeit, it smells like a hack) via the following:
css class current denotes active page.
css class notcurrent denotes in-active page (i.e. page you are not on)
I declared the following method in a class called NavigationTracker.
public static String getCss(String val, String currView)
{
String result = String.Empty;
String trimmedViewName = currView.Substring(currView.LastIndexOf("/") + 1).Replace(".aspx", "");
if (val.ToLower().Equals(trimmedViewName.ToLower()))
result = "current";
else
result = "notcurrent";
return result;
}
I have my stages in a control like this:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%# Import Namespace="TheProject.Models" %>
<link href="../../Content/custom.css" rel="stylesheet" type="text/css" />
<%
String currentView = ((WebFormView)ViewContext.View).ViewPath;
%>
<table width="100%">
<tr>
<td class="<%= NavigationTracker.getCss("LogIn",currentView)%>" style="width:18%;">Log In</td>
<td class="<%= NavigationTracker.getCss("YearSelect",currentView)%>" style="width:18%;">Year Section</td>
<td class="<%= NavigationTracker.getCss("GoalEntry",currentView)%>" style="width:18%;">Goals</td>
<td class="<%= NavigationTracker.getCss("AssessmentEntry",currentView)%>" style="width:18%;">Assessment</td>
<td class="<%= NavigationTracker.getCss("SummaryEntry",currentView)%>" style="width:18%;">Summary</td>
</tr>
</table>
******************* THIS IS WHERE I NEED HELP ***********
To supplement this process, I'd like to create a user control that just has Previous & Next buttons to manage going through this process. So far, one snag I've hit is that this control cannot be put in the master page, but would have to be included in each view, before the form ends. I don't mind that so much. Clicking either the Previous or Next button submit the containing form to the appropriate action; however, I'm unsure on how to do the following:
1) Detect whether the Previous or Next button was clicked
2) Show/Hide logic of Previous & Next buttons at the beginning & end of the process respectively.
Another oddity I'm noticing with my application in general is that, after going through several pages of the process, if I click the back button, some values from my model populate on the page and others do not. For example, the text entered for a text area shows, but the value that had been chosen for a radio button is not selected, yet when inspecting the model directly, the appropriate object does have a value to be bound to the radio button.
I may just need to put that last part in a new question. My main question here is with the navigation control. Any pointers or tips on handling that logic & detecting whether Next or Previous was clicked would be most helpful.
EDIT 1
I had a thought to put a hidden field in the control that displays the Previous & Next buttons. Depending on what button was clicked, I would use JavaScript to update the hidden fields value. The problem now seems to be that the hidden field is never created nor submitted with the form. I've amended the controller post arguments to accept the additional field, but it never gets submitted, nor is it in the FormCollection.
Here is the code for the hidden field. Note that its being generated in a user control that is called inside of the form on the parenting view (hope that makes sense).
<% Html.Hidden("navDirection", navDirection); %>
Thoughts?
EDIT 2
I have resolved all of these issues. I will post the code in detail on Tuesday at the latest. The solution presented below will probably be selected as the answer as that got my on the right thought process.
In short, the solution was to have a Navigation class like the one suggested with logic to determine the next or previous page based on the current view & a string list of all views. A partial view / user control was created to display the Previous / Next buttons. The user control had 2 hidden fields: 1) One with the value of the current view 2) a field indicating navigation direction (previous or next). JavaScript was used to update the hidden navigation field value depending on what button was clicked. Logic in the user control determined whether or not to display the 'Previous' or 'Next' buttons depending on the first and last views in the wizard when compared to the current view.
All said, I'm pretty happy with the results. I'll probably find some code smell issues when I return to this, but, for now, it works.
Thank you all for your help!
EDIT 3 - SOLUTION
Here is the code for the control I built to display the 'Next' & 'Previous' navigation buttons:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%# Import Namespace="Project.Models" %>
<link href="../../Content/custom.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" >
function setVal(val) {
var nav = document.getElementById("NavigationDirection");
nav.value = val;
}
</script>
<%
String currentView = ((WebFormView)ViewContext.View).ViewPath;
String navDirection = "empty";
currentView = NavigationTracker.getShortViewName(currentView);
%>
<input type="hidden" value="<%= currentView %>" name="CurrentView" id="CurrentView" />
<input type="hidden" value="<%= navDirection %>" name="NavigationDirection" id="NavigationDirection" />
<% if( currentView != NavigationTracker.FirstPage) { %>
<div style="float:left;">
<input type="submit" value="Previous" onclick="setVal('previous')" /> <!-- On click set navDirection = "previous" -->
</div>
<% } %>
<% if (currentView != NavigationTracker.LastPage)
{ %>
<div style="float:right;">
<input type="submit" value="Next" onclick="setVal('next')" /> <!-- On click set navDirection = "next" -->
</div>
<% } %>
From there, you render the control just before the closing tag of a form on views you want it like so:
<% Html.RenderPartial("BottomNavControl"); %>
<% } %>
Now I can't really post all of the NavigationTracker code, but the meat of how it works can be deduced from the selected answer and the following snippet that returns the name of the view, based on the current view and the direction (previous or next).
public String NextView
{
get
{
if (String.IsNullOrEmpty(this.NavigationDirection)) return string.Empty;
int index = this.MyViews.IndexOf(this.CurrentView);
String result = string.Empty;
if (this.NavigationDirection.Equals("next") && (index + 1 < MyViews.Count ))
{
result = this.MyViews[index + 1];
}
else if (this.NavigationDirection.Equals("previous") && (index > 0))
{
result = this.MyViews[index - 1];
}
return result;
}
}
Now, doing all of this has a few side effects that could easily be considered code smell. Firstly, I have to amend all of my controller methods that are marked [HTTPPOST] to accept a NavigationTracker object as a parameter. This object contains helper methods and the CurrentView & NavigationDirection properties. Once this is done, I can get the next view the same way in all of my actions:
return RedirectToAction(nav.NextView);
where nav is of type NavigationTracker.
Another note is that the FirstPage & LastPage properties of NavigationTracker are static so I'm actually using NavigationTracker.FirstPage in my global.asax.cs file for my routing. This means I can go to my NavigationTracker class and change the flow in one place for the entire application.
I would love any comments or criticisms on this solution. I admit this might not be a terrific solution, but at first glance, I'm pretty happy with it.
Hope it helps.
You should implement a strong typed view or control. In this type define a property indicating wich step you are and other logic. EX:
public class WizardView
{
public List<string> Steps { get; set; }
public int CurrentStepNumber {get;set;}
public bool ShowNextButton
{
get
{
return CurrentStepNumber < this.Steps.Count-1;
}
}
public bool ShowPreviousButton
{
get
{
return CurrentStepNumber > 0;
}
}
}
And your control:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WizardView>" %>
<table width="100%">
<tr>
<%
int index=0;
foreach(string step in Model.Steps)
{
%>
<td class='<%=Model.CurrentStep==index?"current":"notcurrent" %> style="width:18%;">
<%= step %>
</td>
<%
index++;
} %>
</tr>
</table>
In your controller
public ActionResult MyAction(int step)
{
return View (new WizardControl {
Steps = Myrepository.getSteps();
CurrentStep = step
});
}
}
this may not be exactly what you're looking for, but what if you use jQuery to hide and show DIV tags. Then the user will get the experience of a wizard while all content is all inside a single view. Then it can all be handled nicely with a single controller
Another option might be to create a separate view "step" for each wizard step
http://example.com/page/step/1
Then you can create a Class Object that contains ALL fields, and add to it as you navigate the wizard by adding to a Session Object
(Custom.Class.Obj)Session["myWizard"]
This way it allows you to build standard Views and load what information you have from the Session Object.
Here is another way to go - expanding on the WizardView approach. What you describe is a state engine - something aware of states, transitions and the behaviors and triggers associated with each.
If you check out an implementation like stateless (http://code.google.com/p/stateless/), you'll see that you can desribe the states (steps in the wizard view approach) and the triggers associated with each. Based on your description, you could either capture this information in your state engine wire up - or perhaps ignore it altogether by the fact that all transitions are discretely handled by the engine.
To go a step further, your view wire up can now become quite generic. No need for the view to be aware of the state fulness of the operations, just relying on the view model itself.
Say I want to do the simplest of the data passing as follows :
<% For i = 0 To 10%>
<%Html.RenderPartial("MyUserControl")%>
<% Next%>
What I want to do is to pass the variable i as the parameter to the UserControl so that it displays the number inside a, say, bordered div.
How is this possible?
Thanks
<% For i = 0 To 10%>
<%Html.RenderPartial("MyUserControl", i)%>
<% Next%>
The RenderPartial method has an overload that allows you to pass in a (sub)model. To use it most effectively, your UserControl should be strongly typed - in this case to a model of type System.Int32.
To use it in a UserControl:
<%# Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<System.Int32>" %>
<div><%= this.Html.Encode(this.Model) %></div>
In this case, this.Model is a System.Int32 instance.
you do not. This stuff must be in the model, suppsedly.
PLease upgrade to MVC v2 ;) get rid of the strings.
I thought of a neat hack this morning, probably not original, but I haven't seen it before. I don't consider it good practice, but can help when you need to render a block of code repeatedly around your page, and don't want to touch other code or create other files (partial views or components).
In your .aspx file create an anonymous delegate like so:
<%
Action<DataType> renderMe = data => {
%> Some html text That can contain quotes, etc.
And other <%= data.something %> stuff...
<%
};
%>
Then you can simply use it anywhere you want: (myvar1 and myvar2 are of type DataType)
This is some html and I want the block here: <% renderMe(myvar1); %> ...
or maybe here <% renderMe(myvar2); %>
I know it's not a great idea, but can anyone see any problems with doing this?
This ain't bad per se. Quite similar to Spark's macros (that looks better imho). Rashud`s script manager (for advanced js initialization) uses the same technique. MvcContrib's grid renderer does the same too.
Thing is - niche when this is suitable is really narrow. Only when desired snippet of html should be passed to server side or when you want to re-use it twice or more in context of one specific view but don't want to create separate partial view.
Given two assemblies:
project.web
project.lib
The project.web assembly references project.lib which contains some business logic. A simple class from project.lib:
public class Person
{
public string Name;
}
In project.web.Controllers:
Using project.lib.models;
public class PersonController : Controller
{
Person person = new Person();
public ActionResult Index()
{
return View(person);
}
}
This is the part where I have some questions. In a lot of sample projects I've seen the following in the View (in this case Index.aspx):
<% #Import Namespace="project.lib.models" %>
Allowing you to use the Model object like this:
<%= Model.Name %>
I haven't gotten that to work like the examples, I've had to do:
<%= (Model as Person).Name %>
or
<%
var person = (Person)Model;
Response.Write(person.Name);
%>
Why is this thus? What is the reason for this thusness? Comments? Suggestions? My class definition looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace project.lib.models
{
public class Person
{
public Int64 PersonID;
public string DisplayName;
public string RealName;
}
}
Could you please post your <%# Page %> directive? It should be something like:
<%# Page Language="C#"
[...]
Inherits="System.Web.Mvc.ViewPage<project.lib.models.Person>" %>
Have you tried creating a strongly typed code-behind file for this view instead?
You'd need to update the inherits property of the view to be the code-behind file:
<%# Page Language="C#"
CodeBehind="Index.aspx.cs" Inherits="project.web.Views.People.Index" %>
and then in the code-behind:
namespace project.web.Views.People.Index {
public partial class Index : System.Web.Mvc.ViewPage<project.lib.models.Person>
{}
}
I don't think it's an issue with resolving the System.Web.Mvc.ViewPage class as then the page wouldn't load (you'd get an exception along the lines that System.Web.Mvc.ViewPage cannot be found), and you wouldn't even be able to access the Model property.
Your view may not be strongly typed. Try the following:
Copy the View markup
Delete the View
Go to the controller action that drives the View
Right click within this method, select the Add View option.
Ensure that this is a Strongly typed view and that you properly identify the type of your intended model.
Paste your View markup into the newly created view.
This should work. It doesn't look like you're missing an assembly because you're able to cast the model correctly.
It should be noted that you can do the above without the pain of deleting/restoring a View. Each view has a declaration as its first line that is similar to the following:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<DomainModel.Person>" %>
The important section to understand is the Inherits attribute, which implies that this View extends the MVC ViewPage<DomainModel.Person> class. The Model property will be of type DomainModel.Person at runtime.
Also, there are issues with previous versions of ASP.NET MVC that effectively prevent strongly-typed views from working correctly. See Problem using Add View dialog in ASP.NET MVC RC1 for strongly typed view for more detail.
EDIT: After a bit of detective work by everyone who has posted here, it looks like Visual Studio was referencing older versions of the assembly containing the Person class. Closing the development environment down, clearing out all obj/bin folders, and restarting seems to have fixed this issue.
Update 1: Note that you can be pretty certain that it isn't an issue loading/getting to the types System.Web.Mvc.ViewPage or project.lib.models.Person. As you mentioned in your question, it does work when you use:
<% #Import Namespace="project.lib.models" %>
...
<%= (Model as Person).Name %>
Using System.Web.Mvc.ViewPage<project.lib.models.Person> on the page directive and <%= Model.Name %> must work. Some extra things I would try if that doesn't work:
Remove the import directive when doing so.
Check if you have that namespace added on the web.config.
While none of those 2 above should cause any trouble, if you don't get it to work with what is mentioned before that, it would be a very weird scenario. Also note that if you did add commonly used namespaces in the web.config you can use ViewPage<Person> on the page directive.
The Model.Name you refer to doesn't have to do with importing the namespace, you get it from the type you set in the page directive.
The ViewPage class has the property T Model, which is why you get access to the Name property (you are working with the type you specified).
to do that your view should be strongly typed to your model object in your case the Person class
all you need to make it work is adding refrence to the project.lib and adding updating the page derictive :
<%# Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<Project.lib.Models.Person>" %>