Scaffold DisplayFor instead of EditorFor - asp.net-mvc

I have an asp.net MVC 5 site.
When I scaffold a view it always generates EditorFor on each property.
What data annotation can I use to get it to generate DisplayFor on some fields (as they only need to be displayed not edited)?
Weirdly I can find nothing on Google about this.

You could apply a little trick using the UIHintAttribute.
First, create a new EditorTemplate in your Views\Shared\EditorTemplates folder, with the name DisplayOnly.cshtml, the only content is the line
#Html.DisplayForModel()
Then, mark your Attributes which are never edited but displayed, with the UIHintAttribute:
public class MyViewModel
{
[UIHint("DisplayOnly")]
public string OnylyDisplayed { get; set; }
public string Editable { get; set; }
}
Even if EditoFor(...) is scaffolded, the Template engine will route you to the Display Template.
EDIT
I was sure this would work this way, but I had issued getting it working the first time; interestingly, something like #Html.LabelForModel() in the Template works. Maybe I'm missing something. What will work is, if you need a simple output, to just have #Model in the template. You could use multiple different Editor Templates which act as read only, for different types.

Related

Why is DropDownListFor not recognizing the selected value in my editor template?

I have the following editor template called 'DropDown.cshtml'. The list part works fine, and the template uses some voodoo I did to get the required SelectList from ViewData. The controller places all select lists in the view model into ViewData, and there is nothing wrong with the list side of things.
#{
var list = this.GetModelSelectList();
}
#Html.DropDownListFor(m => Model, list)
I use this template on foreign key view model properties like this one:
[Required]
[UIHint("DropDown", "MVC", "SelectListName", "JobLevelSelectList")]
[Display(Name = "Job Level")]
public Guid? JobLevelId { get; set; }
public SelectList JobLevelSelectList { get; set; }
In the controller, JobLevelId has the correct value immediately before executing the view, yet no item it selected in the rendered select element. or rather, the first item in the select list is always selected.
Why does DropDownListFor ignore the property value when used in my editor template and yet work fine when invoked directly?
This is unfortunately a known bug in MVC3 (I haven't tried it in MVC 4 Beta to see if it fixed).
The work around that I have used is to manually set the Selected property accordingly in collection that the DropDownListFor is bound to, it is not ideal but it worked.

What is the right design pattern for custom template types in ASP.NET MVC?

Here's my situation: I've got a number of specialized object types in my application, and I'm following the standard convention for displaying them with custom templates in the /Shared/DisplayTemplates folder, and editing them with templates in /Shared/EditorTemplates. But I also want to be able to display a custom filter template for each type, so I'd like to add a /Shared/FilterTemplates folder, and implement my own #Html.FilterFor method, so that showing a Filter template is exactly like showing a Display or Editor template.
Does this seem like the best way to handle this situation, or is there a more correct/elegant way to do this in MVC? Thanks in advance.
I'm always using EditorTemplates when data is sent back to server. I assume the user can submit the filter to the server to perform the actual filtering.
When creating filters I prefer to create a model for the filter like:
public class UserListFilterModel
{
public string Username { get; set; }
public bool IsEnabled { get; set; }
}
The view for UserListFilterModel goes into EditorTemplates/UserListFilterModel.ascx.
And then add it as a property on my view model for the page.
public class MyPageViewModel
{
public UserListFilterModel Filter { get; set; }
}
Then I add the filter model to the model for the page and displays it like this:
<%= Html.EditorFor(x => x.Filter)%>
You are probably wrapping the filter in a form to allow the user to submit the values so I think it belongs in EditorTemplates. The users is in fact editing the filter model.
(If you really want to separate them ing you could use the UIHintAttribute but I wouldn't)
Edit: I added some sample code.
I think you misunderstand how Templates work. Templates do not make sense in the context you are describing.
Templates work on a SINGLE data item (although that data item can contain multiple data items, which in turn have their own templates).
The concept of a Filter is to control multiple data items, thus they do not map well to a template.
What you could do is create a DisplayTemplate for your collection class that adds filtering, thus no need to create a custom type of template. Just use DisplayTemplates.

Can you remove the HTML Field Prefix from strongly typed models in MVC 3?

I have a view model like this:
public class EditVM
{
public Media.Domain.Entities.Movie Movie { get; set; }
public IEnumerable<Genre> Genres { get; set; }
}
Movie is the real entity I wish to edit. Genres is simply present to populate a drop down. I would prefer that when I call:
#Html.TextBoxFor(m => m.Movie.Title)
inside my strongly typed view that the input control have a name = "Title" instead of "Movie.Title"
I do not wish to split my view into partial views or lose my strongly typed view by using ViewData or the like.
Is there a way to express to the View that I do not wish to have the Movie. prefix? I noticed that you can set:
ViewData.TemplateInfo.HtmlFieldPrefix = "x";
in the controller, but unfortunately it seems only to allow adding an additional prefix. Setting it to "" does nothing.
Is there any work around for this? Or am I stuck with the unfortunate prefix that isn't really necessary in this case if I wish to keep strongly typed views and lambdas?
Thanks for any help.
Update:
Here's the controller actions to maybe make things a bit clearer.
public ActionResult Edit(int? id)
{
var vm = new EditVM
{
Movie = id.HasValue ? _movieSvc.Find(id.Value) : new Movie(),
Genres = AppData.ListGenres()
};
return View(vm);
}
[HttpPost]
public void Edit([Bind(Prefix = "Movie")]Movie m)
{
_movieSvc.AddOrUpdateMovie(m); //Exceptions handled elsewhere
}
No, in order to do what you want you would have to rewrite the Html helpers, and then you would have to write your own model binder. Seems like a lot of work for minimal gain.
The only choice is a Partial view in which you pass the Movie object as the model. However, this would require you to write your own model binder to have it be recognized.
The reason you have to do m.Movie.Title is so that the ID has the correct name, so the model binder can recognize it as a member of your model.
Based on your update:
Your options are:
Use non-strongly typed helpers.
Use a partial view.
Rewrite the stronly typed helpers
Don't use the helpers at all, and write the values to the HTML
Personally, i'd just use 1 or 2, probably 2.
EDIT:
Based on your update above. Change your code to this (note, Genres does not get posted back to the server, so m.Genres will just be null on postback):
[HttpPost]
public void Edit(EditVM m)
{
_movieSvc.AddOrUpdateMovie(m.Movie); //Exceptions handled elsewhere
}
EDIT:
I did just think of an alternative to this. You could simply do this:
#{ var Movie = Model.Movie; }
#Html.TextBoxFor(m => Movie.Title)
However, if there was a validation error, you would have to recreate your EditVM.
I have a view model like this
I think that you might have some misunderstanding about what a view model is. A view model shouldn't contain any reference to your domain models which is what those Movie and Genre classes seem to be. I mean creating a new class that you suffix with VM and in which you stuff all your domain models as properties is not really a view model. A view model is a class that is specifically designed to meet the requirements of your view.
A much more correct view model would looks like this:
public class EditVM
{
public string MovieTitle { get; set; }
public IEnumerable<GenreViewModel> Genres { get; set; }
}
and in your view you would have:
#Html.EditorFor(x => x.MovieTitle)
#Html.EditorFor(x => x.Genres)
Another option is to either use the TextBox(string name, object value) overload instead of the TextBoxFor:
#Html.TextBox("Title", Model.Movie.Title)
You could also specify the input tag HTML instead of using a helper.
Another option is to take EditVM as your postback parameter. This is what I would do. My post action parameter is always the same type of the .cshtml model. Yes there will be properties like lists that are null, but you just ignore those. It also allows you to gracefully handle post errors as well because if there is an error you'll need to return an instance of that view model anyhow, and have the values they submitted included. I usually have private methods or DB layer that handles retrieving the various lists that go into the ViewModel, since those will be empty on postback and will need to be repopulated, while not touching the properties that were in the post.
With your post method as it is now, if you need to return the same view, you've gotta create a new EditVM and then copy any posted values into it, and still populate the lists. With my method, you eliminate one of those mapping steps. If you are posting more than one thing, are you going to have umpteen different parameters on your post action? Just let them all come naturally into a single parameter typed to the EditVM of the View. While maybe having those null properties in the VM during the postback feels icky, you get a nice predictable consistency between View and postback IMO. You don't have to spend alot of time thinking about what combination of parameters on your post method will get you all the pieces of data from the form.

Can one model be passed through multiple editor templates?

I'm trying to display a view model using an editor template that wraps the model in a fieldset before applying a base Object editor template.
My view:
#model Mvc3VanillaApplication.Models.ContactModel
#using (Html.BeginForm())
{
#Html.EditorForModel("Fieldset")
}
Uses a fieldset template (Views/Shared/EditorTemplates/Fieldset.cshtml):
<fieldset>
<legend>#ViewData.ModelMetadata.DisplayName</legend>
#Html.EditorForModel()
</fieldset>
Which in turn uses a basic template for all objects (Views/Shared/EditorTemplates/Object.cshtml):
#foreach (var prop in ViewData.ModelMetadata.Properties.Where(x =>
x.ShowForEdit && !x.IsComplexType && !ViewData.TemplateInfo.Visited(x)))
{
#Html.Label(prop.PropertyName, prop.DisplayName)
#Html.Editor(prop.PropertyName)
}
That's my intent anyway. The problem is that while the page renders with a fieldset and a legend, the Object template isn't applied so no input controls are displayed.
If I change the view to not specify the "Fieldset" template then my model's properties are rendered using the Object template, so it's not that my Object template can't be found.
Is it possible to pass the same model through multiple templates?
For what it's worth, the view model looks like this:
namespace Mvc3VanillaApplication.Models
{
[System.ComponentModel.DisplayName("Contact Info")]
public class ContactModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
I implemented what you have, and was able to reproduce it. I set a break point in Object.cshtml so I could inspect it and I was caught off guard to realize that it wasn't even hitting the object template when the fieldset template was being used. Then I stepped through the fieldset template and saw it was calling the template just fine, so something must be happening in the code which prevents it from displaying the object template.
I opened up the MVC3 source code, searched for EditorForModel and found the correct function.
public static MvcHtmlString EditorForModel(this HtmlHelper html) {
return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */));
}
Obviously this wasn't it, so I pressed F12 on TemplateHelpers.TemplateHelper, and once there again I pressed F12 on single line call which brings you to the meat of the function. Here I found this short bit of code starting on line 214 of TemplateHelpers.cs:
// Normally this shouldn't happen, unless someone writes their own custom Object templates which
// don't check to make sure that the object hasn't already been displayed
object visitedObjectsKey = metadata.Model ?? metadata.RealModelType;
if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) { // DDB #224750
return String.Empty;
}
Those comments are actually in the code, and here we have the answer to your question: Can one model be passed through multiple editor templates?, the answer is no*.
That being said, this seems like a very reasonable use case for such a feature, so finding an alternative is probably worth the effort. I suspected a templated razor delegate would solve this wrapping functionality, so I tried it out.
#{
Func<dynamic, object> fieldset = #<fieldset><legend>#ViewData.ModelMetadata.DisplayName</legend>#Html.EditorForModel()</fieldset>;
}
#using (Html.BeginForm())
{
//#Html.EditorForModel("Fieldset")
//#Html.EditorForModel()
#fieldset(Model)
}
And viola! It worked! I'll leave it up to you to implement this as an extension (and much more reusable) method. Here is a short blog post about templated razor delegates.
* Technically you could rewrite this function and compile your own version of MVC3, but it's probably more trouble than it's worth. We tried to do this on the careers project when we found out that the Html.ActionLink function is quite slow when you have a few hundred routes defined. There is a signing issue with the rest of the libraries which we decided was not worth our time to work through now and maintain for future releases of MVC.
In first cshtml template we can recreate ViewData.TemplateInfo (and clear VisitedObjects list)
var templateInfo = ViewData.TemplateInfo;
ViewData.TemplateInfo = new TemplateInfo
{
HtmlFieldPrefix = templateInfo.HtmlFieldPrefix,
FormattedModelValue = templateInfo.FormattedModelValue
};
now we can call another template with same model
#Html.DisplayForModel("SecondTemplate")

partialview model updating parent view

not sure how to word this correctly..
so i have a view which has a strongly typed viewmodel as:
class MyViewModel
{
public string MyName get; set;
public string DateOfBirth get; set;
public Address MyAddress get; set;
}
class Address
{
public string Street get; set;
etc...
}
i'm loading the inital view with MyViewModel and using the following to display a partial view to load the address (this way so the address input can be reused).
the view contains a dropdown list with a list of user names in, selecting a value in the drop down calls a ajax .change function which does the following:
$.get('/User/DisplayAddress/', {'selection': selection }, function (html) {
$('#addressBlock').html(html);
});
that all works great..and the html is loaded in.. however, the viewmodel and the address is now disconnected. so when i submit my page, the MyAddress from the ViewModel now contains a null.
How should i be correctly doing this in mvc2 / ajax?
another approach i used was to use the '<% RenderPartial("viewname", Model.MyAddress); %>
which works but i still have to return the data in json and add the values to the fields manually in a java function - this works well.. but its very messy when MyAddress field my contain several fields and to hardcode adding the value into the input fields just looks horrible.
the problem was the binding got screwed when insert the HTML from the partial.
my partial view didnt follow the same namespace so the input fields lost the binding to the parentview model.
i.e. my textbox now has the naming convention MyViewModel.Address.Street etc.
i used the jquery approach to replace the div container with the HTML. the problem was the binding had got screwed when displaying the html.
i set up the 'name' element of the textboxes to use the same as the inputmodel and this kept the binding when inserting the partial.
ideally, i would use a 'flat' viewmodel instead of this 'inheritance' approach but i wasnt able to in this instance.

Resources