I'm working with MVC 3 / Razor for the first time and it seems odd that all the examples and VS scaffolds for create and edit views all have separate HTML views for these concepts.
There is really not much difference between many Create/Edit forms so I was wondering why I can't find examples of people using a single Update form that can be used by both Create and Edit actions.
I have gotten an Update.cshtml view working but was wondering about how it talks to the Edit or Create action method on the controller.
My questions are:
Anyone have a quick answer to talking to the controller, or
Anyone know of a tutorial showing good practice for working this way, or
Is there some good reason for keeping the Create/Edit views separate when the HTML is often the same.
Cheers Dave
This (kind!) of question is asked before: ASP.NET MVC - using the same form to both create and edit
Basically you can create a partial view and include it on your Create and Edit view.
Scott Guthrie has a nice post about Partial Views.
(I've read about this somewhere, but can't find it, I'll update this post when I do find it)
Be mindful that answers to your question should also be driven by business need (and roles). The scaffolding does provide separate functionality, which in some cases is the preferred implementation.
CREATE and EDIT functionality is often pretty much identical from a technical (programming) perspective. This can lead a technical person to think that the functionality should be combined in order to implement a more efficient technical solution. However, any technical implementation must be in response to business need, which might require separation (e.g. by business role) of these concerns.
For example, a business may require that the role which CREATEs business objects is not the same one as EDITs them. In this case, the implemented web pages may not be seen by the same roles (and people) at all.
If you implement CREATE and EDIT using common functionality but the business need is for role separation, you must still implement "role checking" before rendering the required view/partial view/etc. In such cases, separate views can be a preferred implementation.
I do this. I don't know if it's best practice but it can be nice. There are some situations where a completely separate add/edit view could be useful though. Also, if you're using ViewModels then as far as I can tell you're stuck using the same ViewModel for both add and edit. In theory they should both have their own ViewModels.
Here's how this looks for me:
AddVideo.cshtml
#model Multimedia.MediaVideoViewModel
#{
Layout = "~/Views/Shared/LiveSubLayout.cshtml";
}
#section AdditionalHeadContent {
}
<div class="page-header">
<h1>Add a new video</h1>
</div>
<div id="add-video" class="row-fluid">
#Html.Partial("_VideoForm", Model, new ViewDataDictionary { { "ActionKeyword", "Add" } })
</div>
EditVideo.cshtml
#model Multimedia.MediaVideoViewModel
#{
Layout = "~/Views/Shared/LiveSubLayout.cshtml";
}
#section AdditionalHeadContent {
}
#if (ViewBag.Success)
{
<div class="alert alert-success">
<button class="close" data-dismiss="alert">×</button>
<h3><strong>Video saved!</strong></h3><br/>
<div class="btn-group">
Preview this video
#Html.ActionLink("Add Another Video", "AddVideo", "Multimedia", new { Model.Id }, new { #class = "btn" })
#Html.ActionLink("View all media", "Index", "Multimedia", null, new { #class = "btn" })
</div>
<p>or continue editing below...</p>
</div>
}
<div class="page-header">
<h1>Edit video <small>#Model.Title</small></h1>
</div>
<div id="edit-video" class="row-fluid">
#Html.Partial("_VideoForm", Model, new ViewDataDictionary { { "ActionKeyword", "Edit" } })
</div>
_VideoForm.cshtml (partial)
#model Multimedia.MediaVideoViewModel
#{
string actionKeyword = ViewData["ActionKeyword"].ToString();
}
<div class="span6">
#using (Html.BeginForm("editvideo", "multimedia"))
{
<label class="control-label" id="embed-url">Paste video URL here:</label>
<div class="control-group">
#Html.TextBoxFor(model => model.EmbedUrl, new { #class = "span12", id = "video-url", placeholder = "ex: http://www.youtube.com/watch?v=PoAGasPLh30" })
<button class="btn disabled" id="get-video" title="Tooltip">Get Video</button>
</div>
<div class="video-meta">
<h3>Video Information</h3>
<label class="control-label">Title:</label>
<div class="control-group">
#Html.TextBoxFor(model => model.Title, new { #class = "span12", id = "video-title" })
#Html.ValidationMessageFor(model => model.Title, "A title is required", new { #class = "label label-important" })
</div>
<label class="control-label">Description:</label>
<div class="control-group">
#Html.TextAreaFor(model => model.Description, new { #class = "span12", id = "video-description" })
</div>
<h3>Categories</h3>
<div id="tag-search" class="well">
<label class="control-label">Search tags:</label>
<div class="controls"><input type="text" class="typeahead" /></div>
#if (Model != null)
{
foreach (var category in Model.Tags)
{
#Html.Partial("_TagFragment", category)
}
}
</div>
<hr />
#Html.HiddenFor(model => model.Id)
#Html.HiddenFor(model => model.ThumbnailUrl, new { id = "thumb-url" })
<input type="submit" id="video-submit" name="video-submit" class="btn-large btn-primary" value="#actionKeyword video" />
</div>
}
</div>
I edited these down a bit so something might be missing but this should give you the general idea.
here's how i do it, it's not always the best practice (it depends on the case)
1/ combine the controller actions for create and edit
public PartialViewResult Creedit(string id = null)
{
if (id == null)
{
// Create new record (this is the view in Create mode)
return PartialView();
}
else
{
// Edit record (view in Edit mode)
Client x = db.ClientSet.Find(id);
if (x == null) { return PartialView("_error"); }
// ...
return PartialView(x);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Creedit(Client x)
{
if (x.id == null)
{
// insert new record
}
else
{
// update record
}
}
2/ combine the edit and create views into one view i called Creedit
// if you need to display something unique to a create view
// just check if the Model is null
#if(Model==null){
}
so i have 1 view and 2 actions (1 post and 1 get) instead of 2 views and 4 action.
Look into MVC scaffolding in nuget as well, when it generates the view files it does so explicitly creating a creatandedit partial and having the create page and edit page use that partial.
Related
I am still trying to get to grips with Razor Pages for Net Core and seem to be a bit stuck on this. I have my Index.cshtml:
#page
#model IndexModel
<input type="hidden" name="hdnPageSelector" id="hdnIndexPage" />
<div class="text-center">
<p>Welcome to</p>
<h1 class="display-4">"My Web App"</h1>
</div>
<div class="form-row">
<div class="form-group col-md-2">
<partial name="IndexPartials/_Navigation" />
</div>
<div class="form-group col-md-1">
</div>
<div class="form-group col-md-6">
<partial name="IndexPartials/_Body" />
</div>
<div class="form-group col-md-1">
</div>
<div id="refreshMembers" class="form-group col-md-2">
<partial name="IndexPartials/_Members" />
</div>
</div>
Note the last div has an id="refreshMembers".
The partial view (_Members) that is loaded there looks like this:
#model IndexModel
<label>Members</label>
<br />
#{
foreach (ApplicationUser user in Model.AppUsersList)
{
if (user.IsLoggedIn)
{
<label>#user.FirstName #user.LastName </label>
<span class="dot"></span>
}
else
{
<label>#user.FirstName #user.LastName</label>
}
}
}
Within the controller I have a property called:
public IList<ApplicationUser> AppUsersList { get; set; }
And this is populated on OnGetAsync() as follows:
AppUsersList = _userManager.Users.OrderBy(x => x.FirstName).Where(y => y.UserName != currentUser.UserName).ToList();
This is fine, the page loads with the partial view populated as expected. I now want the partial to refresh every 5 seconds so I have put this piece of Javascript/JQuery in place:
$(function () {
setInterval(function () {
$("#refreshMembers").load("/Index?handler=RefreshMembers");
}, 5000);
});
with the following method setup:
public async Task<IActionResult> OnGetRefreshMembers()
{
var currentUser = await _userManager.GetUserAsync(User);
AppUsersList = _userManager.Users.OrderBy(x => x.FirstName).Where(y => y.UserName != currentUser.UserName).ToList();
return new PartialViewResult
{
ViewName = "_Members",
ViewData = new ViewDataDictionary<List<ApplicationUser>>(ViewData, AppUsersList)
};
}
However the partial view doesn't get refreshed. If I put a breakpoint within this method I can see it is being hit every 5 seconds, despite Devtools stating there is an error on each attempt:
In a nut shell, I just can't seem to get my partial view to be reloaded every 5 seconds. It feels like I am close but just missing something and don't know what that is.
Having been reminded to check the Output window in VS a bit better, I found the cause of my problems... Well two things actually. This is the corrected method:
public async Task<IActionResult> OnGetRefreshMembers()
{
var currentUser = await _userManager.GetUserAsync(User);
AppUsersList = _userManager.Users.OrderBy(x => x.FirstName).Where(y => y.UserName != currentUser.UserName).ToList();
return new PartialViewResult
{
ViewName = "IndexPartials/_Members",
ViewData = new ViewDataDictionary<IndexModel>(ViewData, this)
};
}
Where...
I didn't include the folder that the partial lives in when naming it on the PartialViewResult
I need to return the entire IndexModel object - having updated the AppUserList property, and not just the list of AppUsers.
I have a partial view like this (simplified):
#model Portal.Models.LoginModel
<div class="login-container k-block">
<section id="login-form" class="">
#using (Html.BeginForm(actionName, controllerName, new { ReturnUrl = ViewBag.ReturnUrl }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset id="login-form-list-items">
<ol>
<li>
#Html.LabelFor(m => m.CardNumber)
#Html.TextBoxFor(m => m.CardNumber, new { #class="k-textbox"})
<div class="k-error-colored">
#Html.ValidationMessageFor(m => m.CardNumber)
</div>
</li>
<li>
#Html.LabelFor(m => m.Pin)
#Html.PasswordFor(m => m.Pin, new { #class="k-textbox"})
<div class="k-error-colored">
#Html.ValidationMessageFor(m => m.Pin)
</div>
</li>
<input id="login-input-submit" class="k-button" type="submit" value="Enter" />
</fieldset>
</div>
And in my login view I call this partial view like:
#model Portal.Models.LoginModel
#Html.Partial("_LoginFormNoRegistration", Model, new ViewDataDictionary { { "actionName", "Login" }, { "controllerName", "Account" } })
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
The problem is that when the login method in the controller adds an error like:
public ActionResult Login(LoginModel model, string returnUrl)
{
//...
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
The message is not show in the validation summary... I don't understand why... What could be the problem? Some javascript library missing?
Update
I also found that the form generated as the novalidate attribute set:
<form action="/" method="post" novalidate="novalidate">
//...
</form>
I don't know why.
I found the problem.
I was passing a new ViewData in the RenderPartial which was overriding the ViewData of the parent view, so the model state was lost, as explained here: Pass Additional ViewData to an ASP.NET MVC 4 Partial View While Propagating ModelState Errors.
Changing the main view to:
#model Portal.Models.LoginModel
#{
ViewData.Add("actionName", "Login");
ViewData.Add("controllerName", "Account");
Html.RenderPartial("_LoginFormNoRegistration", Model, ViewData);
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Did the trick!
Also, if you want to show a general error message for the model in the validationsummary, be sure to add the error with an empty string as key:
ModelState.AddModelError("error", "The user name or password provided is incorrect."); - doesn't work
ModelState.AddModelError("", "The user name or password provided is incorrect."); - works
Remove the true argument in #Html.ValidationSummary()
It could be a few different things off the top of my head. First off you may not be including the required JavaScript. You may not need all of these but i include these in almost all of my layout views.
<script src="#Url.Content("~/Scripts/jquery-1.8.3.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
Also, could you show the code for your partial view? If you are referencing values that are inside a child class in your model the validation can act a little wonky sometimes.
Lastly, this sounds silly but as you did not post the code for your login model make sure you are using the proper data annotations for the values that you want the validation to show up for.
I'm trying to filter a list of entities and update the partial view on the page with the filtered data. The partial view is returning the correct model with the filtered data, but is not being rendered inside the parent page. Instead it is being rendered in "body" element of an empty HTML page. I've found many topics on this but even though I appear to be following their directions, I'm still having no luck. A fresh set of eyes from the community here may be a huge help.
#model PennLighting.ViewModels.LinecardViewModel
#{
ViewBag.Title = "Linecard";
}
<div class="linecard-head">
#using (Ajax.BeginForm("Index",
new AjaxOptions
{
UpdateTargetId = "linecard"
}))
{
#Html.EditorFor(model => model.Categories)
<div class="buttons">
<input type="submit" name="btnFilter" value="Filter" />
<input type="submit" name="btnShowAll" value="Show All" />
</div>
}
</div>
<div id="linecard">
#Html.Partial("Linecard")
</div>
#section Scripts
{
#Scripts.Render("~/bundles/jqueryval")
}
public ActionResult Index()
{
var viewModel = new LinecardViewModel();
viewModel.Categories = db.Categories
.OrderBy(c => c.Name).ToList();
viewModel.Manufacturers = db.Manufacturers
.OrderBy(m => m.Name).ToList();
return View(viewModel);
}
public ActionResult Index(string btnFilter, string[] selectedCategories)
{
var viewModel = new LinecardViewModel();
var selectedMfrs = new List<Manufacturer>();
if (btnFilter != null && selectedCategories != null)
{
var categoryIds = selectedCategories.Select(c => int.Parse(c)).ToArray();
if (categoryIds != null)
{
selectedMfrs = db.Manufacturers
.Where(m => m.Categories.Any(c => categoryIds.Contains(c.ID)))
.OrderBy(m => m.Name).ToList();
}
}
else
selectedMfrs = db.Manufacturers.OrderBy(m => m.Name).ToList();
viewModel.Manufacturers = selectedMfrs;
return PartialView("Linecard", viewModel);
}
<!DOCTYPE html>
<html>
<head>
<title>#ViewBag.Title</title>
#Styles.Render("~/Content/themes/base/css", "~/Content/css")
</head>
<body>
<div id="container" class="round-bottom">
<div id="header">
<div id="header-left">
<div id="logo">
<a href="#Url.Content("~/")">
<img src="#Url.Content("~/Content/Images/logo.png")" alt="Penn Lighting Associates" /></a>
</div>
</div>
<div id="header-right">
<ul class="nav">
<li>#Html.ActionLink("Home", "Index", "Home")</li>
<li>#Html.ActionLink("About", "Index", "About")</li>
<li>#Html.ActionLink("Linecard", "Index", "Linecard")</li>
<li>#Html.ActionLink("Events", "Index", "Events")</li>
<li>#Html.ActionLink("Gallery", "Index", "Gallery")</li>
<li>#Html.ActionLink("Contact", "Index", "Contact")</li>
<li><a href="http://oasis.pennlighting.com:81/OASIS/desk/index.jsp" target="_blank">
Customer Login</a></li>
</ul>
</div>
</div>
<div id="main">
#RenderBody()
</div>
</div>
<div id="footer">
<p>
Copyright © 2008 Penn Lighting Associates</p>
</div>
#Scripts.Render("~/bundles/jquery")
#RenderSection("scripts",false)
</body>
</html>
So what am I missing? Thanks!
You cannot have 2 actions on the same controller with the same name accessible on the same HTTP verb. You might want to decorate your Index contorller action that is invoked with an AJAX call and returns a partial with the [HttpPost] attribute:
[HttpPost]
public ActionResult Index(string btnFilter, string[] selectedCategories)
{
...
}
Without seeing more of your solution, it's a bit fuzzy, but I believe you want to still return the Index and pass the model data into the Partial in your view. The way you are doing it would return only the partial view, which is why you're getting those results.
So in the filtered index:
return View(viewModel)
And in the index view, pass the data to the partial, which I assume without seeing has the right model association to display.
UPDATE
If you're looking to dynamically pull a subset of data and leave the rest untouched, then do an AJAX POST with the filter information to the action specified for the partial view. Take the data results and place them in the Linecard div.
There are many ways to send the data (bundle by JSON, serialize form, individual data points). Here are some examples:
http://brandonatkinson.blogspot.com/2011/01/using-jquery-and-aspnet-mvc-to-submit.html
MVC ajax json post to controller action method
The problem was that my jqueryval bundle was missing the jquery.unobtrusive-ajax.js file. My code works as is once that was included.
I have a problem with Ajax.
I want to make multiple forms and replace div containing one after submiting with some Partial View. It's working for me when I use only one form. I don't know what I'm doing wrong here. Maybe it's becasue of custom editorfor?
Here is my code, plese try to help me, I've been trying do this for 2 days.
Index
#model MetriceWeb.Models.TaskInputModel
#{
ViewBag.Title = "Tasks";
}
<h3>
Tasks</h3>
#if (Model.Values.Count == 0)
{
<p>No pendings forms for you at the moment</p>
}
else
{
#Html.EditorFor(x => x.Values)
}
TaskInputValue (editorfor)
#model MetriceWeb.Models.TaskInputValue
#{string s = "task" + Model.TaskId;}
#using (Ajax.BeginForm("GetFromLibrary", "Metrice", new AjaxOptions
{
HttpMethod = "Get",
UpdateTargetId = s,
InsertionMode = InsertionMode.Replace
}))
{
<div class="editor-field" id="#s">
#Html.HiddenFor(x => x.TaskId, new { #class = "TaskId" , id = "TaskId" })
#Html.HiddenFor(x => x.InputId, new { #class = "InputId" })
<h2>
#Html.DisplayFor(x => x.Task)
</h2>
<span>Created: </span>
#Html.DisplayFor(x => x.Date)
<div>
Input Value
#Html.EditorFor(x => x.DateValue)
#Html.ValidationMessageFor(x => x.DateValue)
</div>
<input type="submit" value="Submit" />
</div>
<br />
}
It's entering method in my controller where I'm returning partial view. After submitting my div isn't replacing, the whole new site is loaded. What is wrong here? Please help me, I am in big need.
In order to the Ajax helpers like Ajax.BeginForm work correctly (e.g sending an AJAX request) you need reference in your view the following JavaScript file
jquery.unobtrusive-ajax.js
(and of course before that jquery)
From the Unobtrusive Ajax in ASP.NET MVC 3
An interesting note: since there is no actual JavaScript being emitted
when you use unobtrusive Ajax, if you forget to include one or the
other script, you won’t see any errors when attempting to submit the
Ajax request; it will simply behave like a non-Ajax request.
So I have this master/detail setup with a WebGrid. All is well until I try and use RenderPage to display the detail when a record is clicked:
<div class="innerbox">
#{
if(gdEligibility.HasSelection){
#RenderPage("~/Views/Eligibility/EligibilityPolicyDetailView.cshtml",
new { Customer = gdEligibility.SelectedRow })
}
}
</div>
Everything works, if I put a break point I can step through the detail view's cshtml file no problem, no errors. But nothing is ever rendered between the outer div's. Ever. Why doesn't RenderPage return anything? I even tried adding .ToHtmlString() on the end of the line but still nothing.
The detail cshtml:
#{ foreach(TravelInsurance.Models.Policy p in Page.Customer.Policies){
<fieldset>
<legend>Policy</legend>
<div class="display-label">Policy Number</div>
<div class="display-field">
#Html.DisplayFor(model => p.PolicyNumber)
</div>
<div class="display-label">Premium</div>
<div class="display-field">
#Html.DisplayFor(model => p.Premium)
</div>
</fieldset>
}}
This question suggests that RenderPage will use the parent model...
Maybe try using
#{Html.RenderPartial("EligibilityPolicyDetailView", new { Customer = gdEligibility.SelectedRow });}
or
#Html.Partial("EligibilityPolicyDetailView" ,new { Customer = gdEligibility.SelectedRow })
Pl. try again by removing the attribute "ajaxUpdateContainerId" in the grid definition and it should work.