MVC3 Custom HTMLHelper, partial view or other solution to apply DRY principle - asp.net-mvc-3

I've got an MVC3 Read Only view that contains a table displaying properties for an Item.
For many of the properties of the Item, we track the changes a Vendor has made to the item. So, for example, a vendor may update a property named 'Color' from a value of 'Blue' to 'Red'. In this View a table lists each property tracked in a table row, with a column showing the 'Old Value' and the 'New Value'. The next column either shows the current change's status (Awaiting Approval, Approved, or Rejected). However, for Admin users, the column will contain Links ('Approve', 'Reject', or 'Reset to Awaiting Approval').
My markup and Razor code for this is very repetitive and getting out of hand. I'd like to create an HTMLHelper for this, or possibly a partial view that I can use to move all the code into and then use it for each Item Property.
Here is an example of the code used for one Property. This code is repeated for another 10 or so properties.
I'm using some jquery and ajax for the actions. For example, when an change is rejected, the user must enter a reason for rejecting the change.
<tr id="rowId-color">
<td>#Html.LabelFor(model => model.Color)</td>
<td>#Html.DisplayFor(model => model.Color)</td>
#if (Model.ChangeLog != null && Model.ChangeLog.Item("Color") != null) {
var change = Model.ChangeLog.Item("Color");
var changeStatus = (ItemEnumerations.ItemChangeStatuses)change.ItemChangeStatusID;
<td>#change.OldValueDisplay</td>
<td id="tdstatusId-#change.ItemChangeID">
#if (changeStatus == ItemEnumerations.ItemChangeStatuses.AwaitingApproval && User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })
#Html.Raw("|")
<a href="#dialog" name="reject" data-id="#change.ItemChangeID" >Reject</a>
}
else if ((changeStatus == ItemEnumerations.ItemChangeStatuses.Rejected || changeStatus == ItemEnumerations.ItemChangeStatuses.Approved) && User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
}
else {
#changeStatus.ToDisplayString()
}
</td>
<td id="tdreasonId-#change.ItemChangeID">#Html.DisplayFor(m => m.ChangeLog.Item(change.ItemChangeID).RejectedReason)</td>
}
else {
<td colspan="3">No Change</td>
}
</tr>

This really sounds more like a DisplayTemplate for the ItemChangeModel type, that way you can just do:
<tr id="rowId-color">
<td>#Html.LabelFor(model => model.Color)</td>
<td>#Html.DisplayFor(model => model.Color)</td>
#Html.DisplayFor(m => m.ChangeLog.Item("Color"))
</tr>
For each ChangeLog cell and the display template then is like a mini-view with a typed model of ItemChangeModel. So your view file would like like this:
#model ItemChangeModel
#if(Model != null) {
<td>#Html.DisplayFor(m => m.OldValueDisplay)</td>
<td id="tdstatusId-#Model.ItemChangeID">
#switch((ItemEnumerations.ItemChangeStatuses) Model.ItemChangeStatusID) {
case ItemEnumerations.ItemChangeStatuses.AwaitingApproval:
if(User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })
#Html.Raw("|")
<a href="#dialog" name="reject" data-id="#change.ItemChangeID" >Reject</a>
}
break;
case ItemEnumerations.ItemChangeStatuses.Rejected:
case ItemEnumerations.ItemChangeStatuses.Approved:
if(User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
} else {
#changeStatus.ToDisplayString()
}
#break;
}
</td>
<td id="tdreasonId-#change.ItemChangeID">#Html.DisplayFor(m => m.RejectedReason) </td>
} else {
<td colspan="3">No Change</td>
}
(Hard to code in editor box, this could use some cleanup, but I think you will get the idea)
You add this display template (with the file name ItemChangeModel.cshtml) to the Views\Shared\DisplayTemplates folder and it will get used whenever a DisplayFor call is made on that type.
Its been noted in comments that you can't use a method in DisplayFor, but you can change that to an indexed property:
public class ChangeLog
{
public ItemChangeModel this[string key] { get { return Item("Color"); } }
}
Then use:
#Html.DisplayFor(m => m.ChangeLog["Color"])

You haven't shown nor explained how your domain and view models look like but I suspect that what you are using here is not an appropriate view model for this specific requirement of the view. A better view model would have been one that has a list of properties to approve which would be shown in the table.
Anyway, one possible approach is to write a custom HTML helper so that your view looks like this:
<tr id="rowId-color">
#Html.DisplayFor(x => x.Color)
#Html.ChangeLogFor(x => x.Color)
</tr>
...
and the helper might be something along the line of:
public static class HtmlExtensions
{
public static IHtmlString ChangeLogFor<TProperty>(
this HtmlHelper<MyViewModel> html,
Expression<Func<MyViewModel, TProperty>> ex
)
{
var model = html.ViewData.Model;
var itemName = ((MemberExpression)ex.Body).Member.Name;
var change = model.ChangeLog.Item(itemName);
if (change == null)
{
return new HtmlString("<td colspan=\"3\">No Change</td>");
}
var isUserTVAPMgr = html.ViewContext.HttpContext.User.IsInRole("TVAPMgr");
var changeStatus = (ItemChangeStatuses)change.ItemChangeStatusID;
var sb = new StringBuilder();
sb.AppendFormat("<td>{0}</td>", html.Encode(change.OldValueDisplay));
sb.AppendFormat("<td id=\"tdstatusId-{0}\">", change.ItemChangeID);
var ajax = new AjaxHelper<MyViewModel>(html.ViewContext, html.ViewDataContainer);
if (changeStatus == ItemChangeStatuses.AwaitingApproval && isUserTVAPMgr)
{
sb.Append(
ajax.ActionLink(
"Approve",
"Approve",
new {
itemChangeID = change.ItemChangeID
},
new AjaxOptions {
HttpMethod = "POST",
Confirm = "Approve this change?",
OnSuccess = "actionCompleted"
}).ToHtmlString()
);
sb.Append("|");
sb.AppendFormat("Reject", change.ItemChangeID);
}
else if ((changeStatus == ItemChangeStatuses.Rejected || changeStatus == ItemChangeStatuses.Approved) && isUserTVAPMgr)
{
sb.Append(
ajax.ActionLink(
"Reset to Awaiting Approval",
"Reset",
new {
itemChangeID = change.ItemChangeID
},
new AjaxOptions {
HttpMethod = "POST",
Confirm = "Reset this change to Awaiting Approval?",
OnSuccess = "actionCompleted"
}
).ToHtmlString()
);
}
else
{
sb.Append(changeStatus.ToDisplayString());
}
sb.AppendLine("</td>");
sb.AppendFormat(
"<td id=\"tdreasonId-{0}\">{1}</td>",
change.ItemChangeID,
html.Encode(model.ChangeLog.Item(change.ItemChangeID).RejectedReason)
);
return new HtmlString(sb.ToString());
}
}
A better approach would be to re-adapt your view model to the requirements of this view and simply use display templates.

Related

ActionLink to submit Model value

I want my Ajax.ActionLink to pass a viewModel property to action.
Here is my ViewModel
public class ViewModel
{
public string Searchtext { get; set; }
}
My .cshtml
#Ajax.ActionLink("Bottom3", "Bottom3",new { name = Model.Searchtext}, new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "pointsDiv"
})
using(Html.BeginForm("Bottom3", "Home", FormMethod.Get))
{
#Html.TextBoxFor(x => x.Searchtext)
<button type="submit">Search</button>
}
<div id="pointsDiv"></div>
}
My Controller action:
public PartialViewResult Bottom3(string name)
{
var model = db.XLBDataPoints.OrderBy(x => x.DataPointID).Take(3).ToList();
return PartialView("Partial1", model);
}
But the name parameter passed to the action is always null. How do I solve this?
In your code... you have 2 different ways of posting to the server: the link and the form button.
The problem is that the ActionLink has no way to get the value from the input in client side... just the original value.
If you press the Search button, you will see a value posted.
Now, you can use some jQuery to modify a standard ActionLink (not the Ajax.ActionLink):
https://stackoverflow.com/a/1148468/7720
Or... you can transform your Form in order to do a Ajax post instead of a normal one:
https://stackoverflow.com/a/9051612/7720
I did this for a model of mine like so. I ONLY supported the HttpPost method. So add the HttpMethod="POST" to your Ajax.ActionLink
[HttpPost]
public ActionResult Accounts(ParametricAccountsModel model)
{
if (model.Accounts == null)
{
GetAccountsForModel(model);
}
if (model.AccountIds == null)
{
model.AccountIds = new List<int>();
}
return View(model);
}
On the razor view
#Ajax.ActionLink(
"Add Account to Order", "Accounts", "Parametric", null,
new AjaxOptions() { InsertionMode = InsertionMode.Replace, UpdateTargetId = "...", HttpMethod = "POST" },
new { #id = "AddParametricAccountLink" })
The model has a list of selected account ids. So in javascript, I modified the href of the action link dynamically.
function UpdateParametricAccountAction() {
var originalLink = '/TradeNCashMgmt/Parametric/Accounts';
var append = '';
var numberOfRows = $('#ParametricAccounts').find('.parametric-account- row').size();
for (var i = 0; i < numberOfRows; i++) {
if (i != 0) {
append += '&';
}
else {
append = '?';
}
var idValue = $('#NotionalTransactionsAccountId_' + i).val();
append += 'AccountIds%5B' + i + '%5D=' + idValue;
}
$('#AddParametricAccountLink').attr('href', originalLink + append);
}
Since the model binder looks for parameter names in the query string and form submission, it will pick up values using the href. So I posted a model object using the querystring on my Ajax.ActionLink. Not the cleanest method, but it works.

How can I access the values of my view's dropdown list in my controller?

How can I access the values of my view's dropdown list in my controller?
var typeList = from e in db.Rubrics where e.DepartmentID == 2 select e;
var selectedRubrics = typeList.Select(r => r.Category);
IList <String> rubricsList = selectedRubrics.ToList();
IList<SelectListItem> iliSLI = new List<SelectListItem>();
SelectListItem selectedrubrics = new SelectListItem();
selectedrubrics.Text = "Choose a category";
selectedrubrics.Value = "1";
selectedrubrics.Selected = true;
iliSLI.Add(selectedrubrics);
for(int i = 0;i<rubricsList.Count();++i)
{
iliSLI.Add(new SelectListItem() {
Text = rubricsList[i], Value = i.ToString(), Selected = false });
}
ViewData["categories"] = iliSLI;
In my view this works fine showing the dropdown values:
#Html.DropDownList("categories")
Then in my controller I am using FormCollection like this:
String[] AllGradeCategories = frmcol["categories"].Split(',');
When I put a breakpoint here, I get an array of 1’s in AllGradeCategories. What am I doing wrong?
MORE:
Here’s my begin form:
#using (Html.BeginForm("History", "Attendance",
new {courseID = HttpContext.Current.Session ["sCourseID"] },
FormMethod.Post, new { #id = "formName", #name = "formName" }))
{
<td>
#Html.TextBoxFor(modelItem => item.HomeworkGrade, new { Value = "7" })
#Html.ValidationMessageFor(modelItem => item.HomeworkGrade)
</td>
<td>
#Html.TextBoxFor(modelItem => item.attendanceCode, new { Value = "1" })
#Html.ValidationMessageFor(model => model.Enrollments.FirstOrDefault().attendanceCode)
</td>
<td>
#Html.EditorFor(modelItem => item.classDays)
</td>
<td>
#Html.DropDownList("categories")
</td>
}
My controller signature:
public ActionResult ClassAttendance(InstructorIndexData viewModel, int id, FormCollection frmcol, int rows, String sTeacher, DateTime? date = null)
EDITED 2
Tried this but although it seems to get posted, the I still don’t get the values of the list in the hidden field or the categories parameter.
#Html.Hidden("dropdownselected1");
#Html.DropDownList("categories",ViewBag.categories as SelectList, new { onchange = "changed" })
</td>
$(function () {
$("#dropdownselected1").val($("#categories").val());
});
If you are looking to access the selected value from the dropdown list, use a hidden field and repopulate that field on onChange() javascript event of the dropdown list.
but you can use normal
#Html.Hidden("dropdownselected1");
#Html.DropDownList("categories",new { onchange ="changed();"}
function changed()
{
$("#hiddenfieldid").val($("#dropdownlistid").val());
}
should do.
I ended up creating a ViewModel to handle just the drowdown list with an associated partial view. Then in the controller I accessed the values of the list using FormCollection().

Handling concurrency access for deleting an object using Ajax.actionlink

i have the following view inside my asp.net MVC application that contains an ajax.actionlink for deleting objects:-
<table id="incrementanswer">
<tr>
<th>
Description
</th>
<th>
Answer
</th>
<th></th>
</tr>
#foreach (var answer in Model.Answers.OrderBy(a => a.IsRight))
{
<tr id = #answer.AnswersID>
<td>
#Html.DisplayFor(modelItem => answer.Description)
</td>
<td>
#Html.DisplayFor(modelItem => answer.Answer_Description.description)
</td>
<td>
#{ string i = "Are uou sure you want to delete " + #answer.Description.ToString() + " ?";}
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = string.Format(
"deleteconfirmation3({0},{1})",
Json.Encode(answer.AnswersID), Json.Encode(answer.Description))
}) </td>
</tr>}</table>
and the following post delete action method that will be called by the above ajax link:-
[HttpPost]
public void Delete(int id)
{ var a = repository.FindAnswer(id);
repository.DeleteAnswer(a);
repository.Save();}
and the folloiwng OnSuccess script:-
function deleteconfirmation3(rid, rname) {
$('#' + rid).remove();
jAlert(rname + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');}
currently if two user access the same view and then they both click on the delete link which is associated with the same object, then a null exception will be raised on one of the requests; so how i can handle this issue both on the action method side and on the view side to display a friendly message to the user; in these two cases:-
either when the var a = repository.FindAnswer(id); returns a null
exception?
Or when the repository.Save(); does not delete any record?
BR
Edit:-
i updated the post action method to the following:-
[HttpPost]
public ActionResult Delete(int id)
{
try
{
Thread.Sleep(1000);
var a = repository.FindAnswer(id);
repository.DeleteAnswer(a);
repository.Save();
return Json(new { IsSuccess = "True", id = id, description = a.Description }, JsonRequestBehavior.AllowGet);
}
catch (ArgumentNullException) {
return Json(new { IsSuccess = "False" }, JsonRequestBehavior.AllowGet); }}
and on the view i update ajax.actionlink to the folloiwng:-
#{ string i = "Are uou sure you want to delete " + #answer.Description.ToString() + " ?";}
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
//OnBegin = "deleteconfirmation1",
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = "myresponse"
})
and the Onsuccess script:-
function myresponse(data) {
if (data.IsSuccess == "True")
{
$('#' + data.id ).remove();
jAlert(data.description + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');
}
else {
$('#' + data.id).remove();
jAlert(data.description + 'record has already been deleted', 'aaaa');
}
}
The above code worked fine when i test it ,, but does this approach sound acceptable as i have not written something similar to this before?
BR
Add a check such that if the record exists delete it or else show a message specifying that the record has already been deleted...
[HttpPost]
public ActionResult Delete(int id)
{ var a = repository.FindAnswer(id);
if(/*check if a is not null*/){
repository.DeleteAnswer(a);
repository.Save();
return Json(new{ IsSuccess="True", id=id,description=a.Description });
}else{
// display a message record already been deleted
return Json(new{ IsSuccess="False" });
}
}
apparantly you will need to change the return type from void to ActionResult or JsonResult
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = "myResponse"
})
the success handler will be like
function myResponse(data){
if(data.IsSuccess=="True")
jAlert(data.description + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');
else {
alert("record has already been deleted");
}
}

ASP.NET MVC - Current Selected value doesnt get selected in IE

In my Action for Editing an item in my model I have:
ViewBag.PossibleSource = context.Source.ToList();
In my View I have:
#Html.DropDownListFor(model => model.SourceID, ((IEnumerable<btn_intranet.Areas.DayBook.Models.DayBookSource>)ViewBag.PossibleSource).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.SourceName),
Value = option.SourceID.ToString(),
Selected = (Model != null) && (option.SourceID == Model.SourceID)
}))
In Chrome this works as expected. When I pass a model to my view, the current value that's set in my model is the selected value in the list. But in IE8 and 9 it's selected value is the ORIGINAL value my model was set as even though the update does work. So if I selected "hello" originally and then edited to "world". In chrome when i reload the page it will be set to "world" but in IE "hello" is selected in the dropdown even tho "world" is set in my database for my model. It is worth noting these are updated via AJAX
EDIT:
Ajax.Actionlink:
#Ajax.ActionLink(item.ItemNumber, "EditItem", new { id = item.QuoteLineID, enquiryId = item.EnquiryID }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EditItem"
})
This loads the form onto the view.
Ajax.BeginForm:
#using (Ajax.BeginForm("EditItem", new { controller = "QuoteLines" }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "Summary"
}, new { #class = "manual-search cf" }))
{
...Other Model inputs
#Html.DropDownListFor(model => model.SourceID, ((IEnumerable<btn_intranet.Areas.DayBook.Models.DayBookSource>)ViewBag.PossibleSource).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.SourceName),
Value = option.SourceID.ToString(),
Selected = (Model != null) && (option.SourceID == Model.SourceID)
}))
<input type="submit" class="update-items" value="Update Line" />
}
EditItem Action GET request:
public virtual ActionResult EditItem(int id)
{
try
{
DayBookQuoteLines q = context.QuoteLines.Single(x => x.QuoteLineID == id);
ViewBag.PossibleSource = context.Source.ToList();
if (Request.IsAjaxRequest())
{
return PartialView("_EditItem", q);
}
else
{
return RedirectToAction("SalesDetails", new { controller = "Enquiries", id = q.EnquiryID });
}
}
catch (Exception ex)
{
return PartialView("_Error", ex.Message);
}
}
EditItem Action POST request:
[HttpPost]
public virtual ActionResult EditItem(DayBookQuoteLines q)
{
try
{
ViewBag.PossibleSource = context.Source.ToList();
if (ModelState.IsValid)
{
context.Entry(q).State = EntityState.Modified;
context.SaveChanges();
return PartialView("_GetSummary", context.Vehicles.Where(x => x.EnquiryID == q.EnquiryID).ToList());
}
return PartialView("_EditItem", q);
}
catch (Exception ex)
{
return PartialView("_Error", ex.Message);
}
}
I've fixed it, I renamed my GET request for EditItem to EditItemGet and then in my #Ajax.ActionLink I did:
#Ajax.ActionLink(item.ItemNumber, "EditItemGet", new { id = item.QuoteLineID, enquiryId = item.EnquiryID }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EditItem",
HttpMethod = "POST"
})
It was a Cache issue. Thats why it only failed in IE which likes Cache. I read before that making the request a POST request prevents Caching.

How do I process drop down list change event asynchronously?

I have a drop down list that I need to react to asynchronously. I cannot get the Ajax.BeginForm to actually do an asynchronous postback, it only does a full postback.
using (Ajax.BeginForm("EditStatus", new AjaxOptions { UpdateTargetId = "divSuccess"}))
{%>
<%=Html.DropDownList(
"ddlStatus",
Model.PartStatusList.OrderBy(wc => wc.SortOrder).Select(
wc => new SelectListItem
{
Text = wc.StatusDescription,
Value = wc.PartStatusId.ToString(),
Selected = wc.PartStatusId == Model.PartStatusId
}),
new { #class = "input-box", onchange = "this.form.submit();" }
)%>
<div id='divSuccess'></div>
<%
}
When the user selects an item from the list, it does a full postback and the controller method's return value is the only output on the screen. I am expecting the controller method's return value to be displayed in "divSuccess".
[AjaxAwareAuthorize(Roles = "Supplier_Administrator, Supplier_User")]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditStatus(PartPropertiesViewModel partPropertiesViewModel)
{
var part = _repository.GetPart(partPropertiesViewModel.PartId);
part.PartStatusId = Convert.ToInt32(Request.Form["ddlStatus"]);
_repository.SavePart(part);
return Content("Successfully Updated Status.");
}
How about doing this the proper way using jQuery unobtrusively and getting rid of those Ajax.* helpers?
The first step is to use real view models and avoid the tag soup in your views. Views should not order data or whatever. It's not their responsibility. Views are there to display data that is being sent to them under the form of a view model from the controller. When I see this OrderBy in your view it's just making me sick. So define a clean view model and do the ordering in your controller so that in your view you simply have:
<% using (Html.BeginForm("EditStatus", "SomeControllerName", FormMethod.Post, new { id = "myForm" }) { %>
<%= Html.DropDownListFor(
x => x.Status,
Model.OrderedStatuses,
new {
#class = "input-box",
id = "myDDL"
}
) %>
<% } %>
<div id="divSuccess"></div>
and finally in a completely separate javascript file subscribe for the change event od this DDL and update the div:
$(function() {
$('#myDDL').change(function() {
var url = $('#myForm').attr('action');
var status = $(this).val();
$.post(url, { ddlStatus: status }, function(result) {
$('#divSuccess').html(result);
});
});
});
This assumes that in your controller action you would read the current status like this:
[AjaxAwareAuthorize(Roles = "Supplier_Administrator, Supplier_User")]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditStatus(PartPropertiesViewModel partPropertiesViewModel, string ddlStatus)
{
var part = _repository.GetPart(partPropertiesViewModel.PartId);
part.PartStatusId = Convert.ToInt32(ddlStatus);
_repository.SavePart(part);
return Content("Successfully Updated Status.");
}
And when you see this you might ask yourself whether you really need a form in this case and what's its purpose where you could simply have a dropdownlist:
<%= Html.DropDownListFor(
x => x.Status,
Model.OrderedStatuses,
new Dictionary<string, object> {
{ "class", "input-box" },
{ "data-url", Url.Action("EditStatus", "SomeControllerName") },
{ "id", "myDDL" }
}
) %>
<div id="divSuccess"></div>
and then simply:
$(function() {
$('#myDDL').change(function() {
var url = $(this).data('url');
var status = $(this).val();
$.post(url, { ddlStatus: status }, function(result) {
$('#divSuccess').html(result);
});
});
});

Resources