Is it bad practice to have logic in view - asp.net-mvc

I have a table and each item on it has a status or state and depending on this status you can either buy an item or set it in a waiting list.
To do that I have an #if statement in my Razor view where I check the status and then conditionally choose which button to render for the appropriate actions.
I have some similar logic in other views for sessions, and roles.
Is it a good practice? Are there other ways to do it?
#if( #item.status == 1 ) {
<a class="btn btn-default" href="Items/Buy/#item.id">Buy</a>
} else if ( item.status == 2 ) {
<a class="btn btn-default" href="Items/SetList/#item.id">Add To List</a>
}

It is not the worst thing in the world, since it is being used to display elements in the view. However I would make an extension method for the HtmlHelper to render the action links you are trying to create. Something like:
public static class HtmlHelperExtensions
{
public static HtmlString RenderStatus(this HtmlHelper helper, ItemModel item)
{
if (item.status == 1)
{
return helper.ActionLink("Buy", "Buy", "Items", new { #class = "btn btn-default" });
}
if (item.status == 2)
{
return helper.ActionLink("Add To List", "SetList", "Items", new { id = item.Id }, new { #class = "btn btn-default" });
}
return new MvcHtmlString("");
}
}
Then in your view, all you would need to do is call
#Html.RenderStatus(item)

There isn't anything wrong with the way you've done this.
When you hear or read about the idea that it is 'bad practice to include logic in your views' it is typically referring to business logic.
There is nothing that prevents one from passing a model into a view that has all kinds of methods available on it, that can then be executed from code in a view -(but this is what you should avoid -- instead getting this done in the controller action).
The logic that you have shown is really rendering logic -- you are conditionally choosing the appropriate HTML to include in your output. No problem at all in my opinion.
If this logic gets too cumbersome in one or more views you can use HTML helpers, PartialViews, and functions in Razor to help with that problem.

While I fundamentally agree with the fact that what you're doing is technically rendering logic, determining whether or not something can be bought or added could be considered business logic.
Also, you might have items other than the buttons that depend on that logic to be displayed or active or something.
I would suggest the following approach:
Define one or more bool properties on your view, such as CanBuy.
The value of the property can be set in the controller or in the property definition in the view, like so
public bool CanBuy
{
get { return (status == 1); }
}
Then in your view you would simplify the if statement like this:
#if(#item.CanBuy ) {
<a class="btn btn-default" href="Items/Buy/#item.id">Buy</a>
} else if ( item.CanAdd ) {
<a class="btn btn-default" href="Items/SetList/#item.id">Add To List</a>
}
In addition, Bobby Caldwell's suggestion to abstract this in a HtmlHelper is valuable and could be combined with this method.

Related

Model not updated after database change

Im having problem that it seems my model not updated after database change. I have a list that each have button to approve or reject.
using (Html.BeginForm("LeaveProcess", "Admin", FormMethod.Post, new { #class = "form-horizontal" }))
{
<input type="text" name="LvAppId" hidden value="#item.LvAppId">
<li><button type="submit" class="btn btn-link" name="AppVal" value=true data-confirm="are u sure?" >Approve</button></li>
<li><button type="submit" class="btn btn-link" name="AppVal" value=false >Reject</button></li>
}
When i press approve or reject button, the value of that submit button is go to controller
[HttpPost]
public ActionResult LeaveProcess(string LvAppId, bool AppVal)
{
DataChange.UpdateStatusFromAdmin(LvAppId, AppVal);
TempData["AlertMessage"] = "Your Action Has Been Processed";
return RedirectToAction("Index");
}
and update database column to "approved" or "reject".
public static void UpdateStatusFromAdmin( string LvAppId, bool AppVal)
{
if (AppVal == true)
{
db.Database.ExecuteSqlCommand("UPDATE LeaveApps SET Status='Approved' WHERE LvAppId={0}", LvAppId);
}
else
{
db.Database.ExecuteSqlCommand("UPDATE LeaveApps SET Status='Rejected' WHERE LvAppId={0}", LvAppId);
}
}
The database is updated (checked it with ssms), but when i reload the page, the value still not updated and i must restart debugging in visual studio to make it updated, what should i do? Thanks
If you are confident that that the query is correct and you can see changes being made to the backend using ssms but the changes aren't being reflected in your app.. I think that is a good indication that there is a problem with the context.
From your code it looks like you aren't disposing of the context properly.
Wrapping your data access inside a using statement like so:
using(var db = new DatabaseContext())
{
// perform data access here
}
Will automatically dispose of it...
See https://msdn.microsoft.com/en-gb/data/jj729737.aspx
If that doesn't solve your problem I'd then focus on your query
Besides writing your values to the database, you do nothing with them.
Create an object of your model and call your view.
var model = new YourModel
{
LvAppId = lvAppId,
AppVal = appVal
};
return View("Index", model);
Otherwise you have to make a call to the database to get your most recent values

Partial view - avoiding nested forms

View
#using (Html.BeginForm())
{
...form elements
#Html.Action("PartialView")
}
PartialView
if (something) {
<input type="submit" value="Submit" />
} else {
#using (Html.BeginForm())
{
<input type="submit" value="Submit" />
}
Can anybody suggest a way around the above problem?
If the PartialView if statement returns false, I end up with nested forms. I can move the form close bracket within the partial view to avoid nesting the forms and the page renders correctly but this upsets visual studio because it expects to see the close bracket within the view. Does that matter?
Edit:
Based on Chris's comments, is the below modification a better approach? i.e. One form with two submit buttons that call different code within the same action method?
PartialView
if (something) {
<input type="submit" name="btn" value="Submit1" />
} else {
<input type="submit" name="btn" value="Submit2" />
}
Controller
[HttpPost]
public ActionResult Index()
{
if (btn == "Submit1") {
...do a thing
} else {
...do another thing
};
}
<form> tag inside another <form> is not a valid HTML
Refer W3c Spec
Workaround available
http://blog.avirtualhome.com/how-to-create-nested-forms/
I run into the same problem, and came up with a helper that really solves it.
/**
* Ensure consequent calls to Html.BeginForm are ignored. This is particularly useful
* on reusable nested components where both a parent and a child begin a form.
* When nested, the child form shouldn't be printed.
*/
public static class SingleFormExtensions
{
public static IDisposable BeginSingleForm(this HtmlHelper html)
{
return new SingleForm(html);
}
public class SingleForm: IDisposable
{
// The form, if it was needed
private MvcForm _form;
public SingleForm(HtmlHelper html)
{
// single per http request
if (!HttpContext.Current.Items.Contains(typeof(SingleForm).FullName))
{
_form = html.BeginForm();
HttpContext.Current.Items[typeof(SingleForm).FullName] = true; // we need the key added, value is a dummy
}
}
public void Dispose()
{
// close the form if it was opened
if (_form != null)
{
_form.EndForm();
HttpContext.Current.Items.Remove(typeof(SingleForm).FullName);
}
}
}
}
To use it, include the namespace of the extension and do #Html.BeginSingleForm( everywhere you want. Not just inside nested views, but also in the parent.
Points of interest: There is a need to save whether or not the form has opened earlier. We can't have a static or static per thread variable ThreadStatic, as this might be used by many Asp.Net threads. The only single-threaded and per-http-request place to add a variable is the HttpContext.Current.Items dictionary.
There is no limitation to the number of submit buttons. The problem is the nested form elements. To avoid having multiple submit buttons you can either hide them using jquery, or extend this helper to automatically add a submit button at the end.

Conditional ASP.NET MVC razor sections

I want to define this section only if some property (Model.ReadOnly) is false.
#section toolbar {
<div class="tool">
<div class="row">
#Html.ActionLink( Resources.Strings.Edit, "Edit", "Profile" )
</div>
<div class="row">
#Html.ActionLink( Resources.Strings.Delete, "Delete", "Profile" )
</div>
</div >
}
I tried wrapping it up in #if ( !Model.ReadOnly ) {} but it doesn't work.
Is there a way to do this?
I do not want to define an empty section (as #itsmatt suggests), the layout of my page changes whether the section is defined or not (using IsSectionDefined( "toolbar" )).
This should work.
#if (!Model.ReadOnly)
{
<text>
#section toolbar {
}
</text>
}
I never said it would be pretty ;-)
This works for me:
#section SomeSection {
#if (!Model.ReadOnly)
{
}
}
Essentially flipping where the conditional is. This essentially results in an empty section if Model.ReadOnly is true.
Update:
So, what about moving that section to a PartialView and doing something like:
#Html.Partial("MyAction")
in your View and then let the MyAction return you the appropriate PartialView based on the ReadOnly value? Something like:
public PartialViewResult MyAction()
{
...
// determine readonly status - could have passed this to the action I suppose
if (ReadOnly)
{
return PartialView("TheOneThatDefinesTheSection");
}
else
{
return PartialView("TheOneThatDoesNotDefineTheSection");
}
}
Seems like that would work just fine.
Bertrand,
See:
Razor If/Else conditional operator syntax
basically (para-phrasing), ... Razor currently supports a subset of C# expressions without using #() and unfortunately, ternary operators are not part of that set.
also, this may be a way around the issue:
conditional logic in mvc view vs htmlhelper vs action
basically, use the if logic to call a partialview to satisfy your criteria.
[edit] this was my basic thinking (where your #section code is defined in that partial):
#if(!Model.ReadOnly)
{
#Html.Partial("toolbar")
}

asp.net mvc Ajax.BeginForm clone

I'm using asp.net mvc ajax.
The partial view is using Ajax.BeginForm (just an example):
<div id="divPlaceholder">
<% using (Ajax.BeginForm(new AjaxOptions { UpdateTargetId = "divPlaceholder" })) { %>
... asp.net mvc controls and validation messages
<input type="submit" value="Save" />
<% } %>
</div>
After update, if validation fails, the html is:
<div id="divPlaceholder">
<div id="divPlaceholder">
...form
</div>
</div>
I don't like that the returned html is inserted, instead it should replace original div.
Probably on POST I should not render <div> around form in partial view or render the div without id.
What else can I do in this situation?
I was thinking that maybe I should write a helper, something like Ajax.DivBeginForm, which will render form inside div on GET and hide the div on POST.
Can somebody provide a good advice how to write such helper (Ajax.DivBeginForm)?
I'd like it to work with using keyword:
<% using (Ajax.DivBeginForm(new AjaxOptions { UpdateTargetId = "myId" })) { ... }%>
My solution. Please comment if something is wrong.
public class DivMvcForm : MvcForm
{
private bool _disposed;
private MvcForm mvcForm;
private ViewContext viewContext;
public DivMvcForm(MvcForm mvcForm, ViewContext viewContext) : base(viewContext)
{
this.mvcForm = mvcForm;
this.viewContext = viewContext;
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
mvcForm.EndForm();
viewContext.Writer.Write("</div>");
}
}
}
Helper
public static class AjaxHelperExtensions
{
public static MvcForm DivBeginForm(this AjaxHelper ajaxHelper, AjaxOptions ajaxOptions)
{
var tagBuilder = new TagBuilder("div");
if (ajaxHelper.ViewContext.HttpContext.Request.RequestType == "GET"
&& string.IsNullOrWhiteSpace(ajaxOptions.UpdateTargetId) != true)
{
tagBuilder.MergeAttribute("id", ajaxOptions.UpdateTargetId);
}
ajaxHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
var theForm = ajaxHelper.BeginForm(ajaxOptions);
return new DivMvcForm(theForm, ajaxHelper.ViewContext);
}
}
And how it works
<% using (Ajax.DivBeginForm(new AjaxOptions { UpdateTargetId = "divPlaceholder" })) { %>
... controls
<% } %>
Result - when ModelState is invalid the partial view returns div without id.
I'm going to take a little different approach here, rather than getting your original solution to work, I'd recommend using the pattern normally followed in this scenario and not using a helper. I realize this is a bit later than the original post, but for future use by anyone : )
If your partial view has a form, then you will keep posting, and returning a form in a form in a form in a form, etc. so you want to have your PARENT contain BeginForm, the div, and renderpartial
using (Ajax.BeginForm("Index", "ProjectManager", new AjaxOptions() ....
<div id="divPlaceholder">
Html.RenderPartial(....)
</div>
If you want to encapsulate this logic in say, an "Order" partial view that is displayed on a Customer screen, then you have two options.
1. Include the BeginForm on the parent Customer view (which reduces code reusability as any view that wants to include the "Order" partial view must include the ajax wiring.
Or
2. You have two partial views for order. One is OrderIndex.ascx (or cshtml if razor) and one is OrderIndexDetail.ascx (or whatever naming convention you decide)
OrderIndex contains your Ajax.beginform and OrderIndexDetail has no form, only the partial view details.
Option 2 is more code (ok, literally about 30 more seconds of coding to move the ajax.beginform into another view) but increases code reusability.
You can handle submit form yourself:
<div id="divPlaceholder">
<% using (Html.BeginForm("action", "controller", FormMethod.Post, new { id = "submitForm"})) { %>
... asp.net mvc controls and validation messages
<input type="submit" value="Save" />
<% } %>
</div>
and write some javascript as:
$('#submitForm').submit(function() {
$.post('post-to-this-url',
data: { foo: formvalue1, bar: formvalue2},
function(data) {
// update html here
});
return false;
})

how to render a full view using Ajax.BeginForm

I have a partial view which has a Ajax.BeginForm, with a UpdateTargetID set. When the validation on the form fails the update target id is replaced with the validation errors, but when there are no validation errors users should be redirected to a new page.
The code in my Partial view is
<div id="div_UID">
<% using (Ajax.BeginForm("FindChildByUID", new AjaxOptions { UpdateTargetId = "div_UID" } ))
{%>
<p>
<label>UID:</label>
<%= Html.TextBox("UID") %>
</p>
<input type="submit" value="Continue" />
<% } %>
</div>
</pre>
The code in my controller is as follows
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult FindChildByUID(Student student)
{
Student matchingStudent = _studentService.FindChildByUID(student.UID);
if (matchingStudent == null)
{
ModelState.AddModelError("UID", String.Format("No matching child found for the entered UID: {0}", student.UID));
return PartialView();
}
else
{
// full view
return RedirectToAction("ConfirmChildDetails", matchingStudent);
}
}
So, for I have been unsuccessful to display the full view on it's own, as it always seems to dipslay the full view in the UpdateTargetID div specfied in the Ajax.BeginForm.
Any suggestions on how I can get this to work?
Thanks
What your AJAX post is doing is making a request and waiting on a response that contains html to input onto the page. The configuration is such that whatever html is returned will be injected into the div you've named "div_UID".
I typically avoid scenarios like this and use traditional posting if a redirect is required upon a successful outcome of the POST.
I imagine you could do it like this using jQuery to submit rather than the Ajax.BeginForm (or just set a callback function for your Ajax.BeginForm):
function SubmitForm(form) {
$(form).ajaxSubmit({ target: "#div_to_update", success: CheckValidity });
}
function CheckValidity(responseText) {
var value = $("#did_process_succeed").val();
if (value == "True") {
window.location.replace("url_of_new_action_here");
}
}
You just have to have a hidden field in your partial view called "did_process_succeed" and set the value of True or False based on some logic in your controller.
There are likely other ways as well. Perhaps someone else will chime in. I hope this helps for now.

Resources