Been picking up MVC4 and Razor and having a ball, but I've got a question on the approach for what I want to acheive:
I have a page with some panels on (Like a dashboard), and a set of icons you can drag and drop to these panels to 'install' a module into that panel, and display it's content. This is great from a UI point of view, now I'm looking at hooking this up to something a bit meatier:
What I have:
IContentModule
Set of concrete classes for each module with a Render() method
Controller that handles module drop event and an Activator to get an instance of the class for that module drop
Simple stuff really, ideally, I want it so that each module is responsible for it's own content, but aside from having a string return from Render, is there a better way, like, assigning a specific view markup to that particular concrete class, so that I can have control over what is being rendered, but in a much more structured way, wondering what the best approach is here?
Thanks for your time!
Danny
Edit: Sorta thinking if there was a way to couple a view to my concrete classes? e.g. ViewForum.cshtml binding to ForumModule.cs, somehow instantiating the view and getting a string from it's render of the object, then passing that back via a string to insert into my panel?
An example of a panel:
<section class="main box droppable" id="MainPanel">
<div class="padding">
Panel 1
</div>
</section>
The jQuery event
$(".droppable").droppable({
hoverClass: 'boxhover',
drop: function (event, ui) {
$.ajax({
type: "POST",
url: '/Home/AddModule/' + $(ui.draggable).attr("id") + "?returnTo=" + this.id,
success: function(data) {
$("#" + data.Target).html(data.Content);
}
});
}
});
The controller method
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult AddModule(string id, string returnTo)
{
string content = DemoResolve(id);
try
{
IContentModule module = (IContentModule) Activator.CreateInstance(Type.GetType("Foo.Bar.BLLForumModule,Foo.Bar"));
content = module.Render();
}catch(Exception exp)
{
throw;
}
return Json(new { Target = returnTo, Content = content });
}
So where i have that module.Render(), i'm thinking I'd want to get a partial view or something and render that based on the object I have in hand
Worked with a colleague of mine and came up with a solution of binding the views and the modules together in a sort of dynamic way, for example, if the jQuery post comes back with the string of the module 'BLLForumModule' we have the following:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult AddModule(string id, string returnTo)
{
string content = DemoResolve(id);
try
{
content = RenderView("BLLForumModule");
}catch(Exception exp)
{
throw;
}
return Json(new { Target = returnTo, Content = content });
}
private string RenderView(string moduleName)
{
string result = "";
IContentModule module = (IContentModule)Activator.CreateInstance(Type.GetType("Foo.Bar." + moduleName +",Foo.Bar"));
this.ViewData.Model = module;
using (var sw = new System.IO.StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines
.FindPartialView(this.ControllerContext, moduleName);
var viewContext = new ViewContext(this.ControllerContext,
viewResult.View, this.ViewData, this.TempData, sw);
viewResult.View.Render(viewContext, sw);
result = sw.GetStringBuilder().ToString();
}
return result;
}
This assumes there is a class in the Foo.Bar assembly with the same name as the view we are trying to load (BLLForumModule.cshtml and Foo.Bar.BLLForumModule.cs)
I then take the rendered content from the view and spit it back as a JsonResult as the Content part and the Target part of the JsonResult as the ID of the panel it needs to be dropped into.
This feels pretty good I think, any suggestions or improvements welcome!
Related
Question background:
I am implementing some basic 'shopping cart' logic to an MVC app. Currently when I click a link - denoted as 'Add To Cart' on the screen shot below this calls to an 'AddToCart' method in the 'ProductController' as shown:
Product.cshtml code:
#Html.ActionLink("Add To Cart", "AddToCart")
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
The issue:
Not an issue as such but currently when I click the 'Add To Cart' button on the ActionLink on the ProductDetail.cshtml view the page calls the 'AddToCart' method on the ProductController and gives a blank view on the page - as shown below. I want the view to stay on 'ProductDetail.cshtml' and just call the 'AddToCart' method, how do I do this?
Basically #Html.ActionLink() or <a></a> tag uses get request to locate the page. Hence whenever you clicked it, you request to your AddToCart action method in ProductController and if that action method returns null or void so a blank or empty page is shown as you experienced (because or #Html.ActionLink() get request by Default).
So if you want to add your value to cart then call AddToCart method using ajax i.e:
HTML:
#Html.ActionLink("Add To Cart", "AddToCart", null, new { id="myLink"})
Jquery or Javascript:
$("#myLink").click(function(e){
e.preventDefault();
$.ajax({
url:$(this).attr("href"), // comma here instead of semicolon
success: function(){
alert("Value Added"); // or any other indication if you want to show
}
});
});
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
Now this time when the call goes to AddToCart method it goes by using ajax hence the whole page will not redirect or change, but its an asynchronous call which execute the AddToCart action method in your ProductController and the current page will remains same. Hence the product will also added to cart and page will not change to blank.
Hope this helps.
The answer of Syed Muhammad Zeeshan is what you are looking for, however you may return an EmptyResult.
public ActionResult AddToCart()
{
//Logic to add item to the cart.
return new EmptyResult();
}
According to this it has no impact on your code ASP.Net MVC Controller Actions that return void
But maybe sometime you want to return data and then you could do something like this:
if (a)
{
return JSon(data);
}
else
{
return new EmptyResult();
}
As many people mentioned here you will need to use AJAX if your using asp.net MVC to hit a controller POST function without having to leave your view.
A good use case for this is if you want to upload a file without refreshing the page and save that on the server.
All of the
return new EmptyResult();
Wont work, they will still redirect you.
Here is how you do it, in your view have the follow form as an example:
<form enctype="multipart/form-data" id="my-form">
<p>
The CSV you want to upload:
</p>
<input type="file" class="file-upload" name="FileUpload" />
</div>
<div>
<button type="submit" class="btn btn-default" name="Submit" value="Upload">Upload</button>
</div>
</form>
Then in the JavaScript side you need to add this to your view with within Script tags.
$("#my-form").on('submit', function (event) {
event.preventDefault();
// create form data
var formData = new FormData();
//grab the file that was provided by the user
var file = $('.file-upload')[0].files[0];
// Loop through each of the selected files.
formData.append('file', file);
if (file) {
// Perform the ajax post
$.ajax({
url: '/YourController/UploadCsv',
data: formData,
processData: false,
contentType: false,
type: 'POST',
success: function (data) {
alert(data);
}
});
}
});
Your controller might look something like this to process this type of file:
[HttpPost]
public void UploadCsv()
{
var listOfObjects = new List<ObjectModel>();
var FileUpload = Request.Files[0]; //Uploaded file
//check we have a file
if (FileUpload.ContentLength > 0)
{
//Workout our file path
string fileName = Path.GetFileName(FileUpload.FileName);
string path = Path.Combine(Server.MapPath("~/App_Data/"), fileName);
//Try and upload
try
{
//save the file
FileUpload.SaveAs(path);
var sr = new StreamReader(FileUpload.InputStream);
string csvData = sr.ReadToEnd();
foreach (string r in csvData.Split('\n').Skip(1))
{
var row = r;
if (!string.IsNullOrEmpty(row))
{
//do something with your data
var dataArray = row.Split(',');
}
}
}
catch (Exception ex)
{
//Catch errors
//log an error
}
}
else
{
//log an error
}
}
There are many ways to accomplish what you want, but some of them require a lot more advanced knowledge of things like JavaScript than you seem aware of.
When you write ASP.NET MVC applications, you are required to have more intimate knowledge of how browsers interact with the web server. This happens over a protocol called HTTP. It's a simple protocol on the surface, but it has many subtle details that you need to understand to successfully write ASP.NET MVC apps. You also need to know more about Html, CSS, and JavaScript.
In your case, you are creating an anchor tag (<a href="..."/>), which when click upon, instructs the browser to navigate to the url in the href. That is why you get a different page.
If you don't want that, there are a number of ways change your application. The first would be, instead of using an ActionLink, you instead simply have a form and post values back to your current controller. And call your "add to cart" code from your post action method.
Another way would be have your AddToCart method look at the referrer header (again, part of that more subtle knowledge of http) and redirect back to that page after it has processed its work.
Yet another way would be to use Ajax, as suggested by Syed, in which data is sent to your controller asynchronously by the browser. This requires that you learn more about JavaScript.
Another option is to use an embedded iframe and have your "add to cart" be it's own page within that iframe. I wouldn't necessarily suggest that approach, but it's a possibility.
Controller should return ActionResult. In this case a redirect to the caller page.
using System.Web.Mvc;
using System.Web.Mvc.Html;
public ActionResult Index()
{
HtmlHelper helper = new HtmlHelper(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Index"), new ViewDataDictionary(), new TempDataDictionary(), new System.IO.StringWriter()), new ViewPage());
helper.RenderAction("Index2");
return View();
}
public void Index2(/*your arg*/)
{
//your code
}
I was struggling with this and couldn't get it working with ajax.
Eventually got a working solution by making my controller method return type ActionResult rather than void and returning a RedirectToAction() and inputting the action relating to the view I wanted to remain on when calling the controller method.
public ActionResult Method()
{
// logic
return RedirectToAction("ActionName");
}
Working with Multiple Steps on a form and different views sharing one model.
On my second Step Form Post submit I am passing in my model to maintain the state.
<% using (Html.BeginForm("StepTwo", "Home", Model, FormMethod.Post, new { id = "restrictionForm" })) { %>
But I find when I hit submit and it works the way I want, my entire model and its attributes are now listed in the url which is not desirable.
[Authorize]
[HttpPost]
[ActionName("StepTwo")]
[ValidateAntiForgeryToken]
public ActionResult StepTwoPost(PostcodesModel model)
{
try
{
_Provider.AddNewRestriction(model.Postcode, model.SelectedRestriction, model.RestrictionDescription, model.WildcardID);
return View("StepThree", model);
}
catch(Exception ex)
{
ViewBag.PostCodeErrors = "<div class=\"errorMessage\">Error inserting restriction " + model.Postcode + ".</div><p>" + ex.Message + "</p>";
return View(model);
}
}
Anyway to now display model in URL this way ?
i.e.
http://localhost:/Home/StepThree?Postcode=1234&.....
can you post the complete view?,
how are are you rendering your Models values? if you are rendering them as routing data within the submit button these will cause them to appear on the URL, you should render them the model within HTML elements "Textbox" or "Hidden" for example and these values will be posted as form data
Been working on creating an interface to allow a modular approach to the UI, the background:
Allows users to drag and drop a module onto a div
jQUery posts back to controller with the module and panel names
Controller returns a JsonResult containing a view that has been rendered, specific to that module
Here is a picture of the UI so you can sort of see what I am doing:
Image
Now, what I am trying to do, is in that JsonResult (Which contains a string output of a view rendering), is save some data back to the model, and refresh that dynamically rendered view, so that just the panel (Where the view has been rendered) updates.
Sounds complicated i know, so here is some code:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult AddModule(string id, string returnTo)
{
string content = RenderView(id);
return Json(new { Target = returnTo, Content = content });
}
private string RenderView(string moduleName)
{
string result = "";
ContentModule module = (ContentModule)Activator.CreateInstance(Type.GetType("TrustMRM.BLL.ContentModules." + moduleName + ",TrustMRM.BLL"));
module.TrustID = Settings.Default.TrustID;
module.DataBind();
this.ViewData.Model = module;
using (var sw = new System.IO.StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(this.ControllerContext, moduleName);
var viewContext = new ViewContext(this.ControllerContext, viewResult.View, this.ViewData, this.TempData, sw);
viewResult.View.Render(viewContext, sw);
result = sw.GetStringBuilder().ToString();
}
return result;
}
The above is what handles the 'drop' of the module. I have an abstract class, ContentModule, and an implementation called BLLForumModule, there is a matching view, BLLForumModule.cshtml, that gets built, and returned in that string, strongly bound tot he BLLForumModule.
What is rendered is a drop down list, equal to some data to configure that particular module:
#model TrustMRM.BLL.ContentModules.BLLForumModule
#{
Layout = null;
}
#if (Model.IsConfigured)
{
<span>I am configured</span>
}
else
{
using (Html.BeginForm("RefreshModule", "Home"))
{
<h3 class="panelHeader">#Html.DisplayTextFor(m => m.Title)</h3>
<span>Select group</span>
#Html.DropDownListFor(m => m.SelectedGroupID, Model.GroupSelection.Select(t => new SelectListItem { Text = t.GroupName, Value = t.GroupID.Value.ToString() }));
#Html.HiddenFor(x => x.ModuleID);
<input type="submit" value="Ok" />
}
}
Now, I am unsure of what to return, or how to handle this post in order to refresh that view, the one that was rendered as a string and sent back, any insight into this, and if anyone has done something similar before, perhaps my rendering the view to a string is the wrong approach?
The code to accept the form post:
public ActionResult RefreshModule(string ModuleID)
{
return View();
}
(Doesn't work)
Something like that will help you
Using Ajax.BeginForm with ASP.NET MVC 3 Razor
Just use Ajax.BeginForm and provide an id of replaced element.
Attach validation after ajax request here
MVC3 Unobtrusive Validation Not Working after Ajax Call
Goal:
I want to be able to type URL: www.mysite.com/NewYork OR www.mysite.com/name-of-business
Depending on the string I want to route to different actions without changing the URL.
So far I have:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"UrlRouter", // Route name
"{query}", // URL with parameters
new { controller = "Routing", action = "TestRouting" } // Parameter defaults
);
}
In the controller I have:
public ActionResult TestRouting(string query)
{
if (query == "NewYork")
return RedirectToAction("Index", "Availability"); // <--------- not sure
else if (query == "name-of-business")
return Redirect("nameofbusines.aspx?id=2731"); // <--------- not sure
else
return RedirectToAction("TestTabs", "Test"); // <--------- not sure
}
I have pretty much tried everything to redirect/transfer to the page without
changing the URL, but everything I've tried changes the URL or gives me an error.
Basically I'm looking for the equivalent of server.transfer where I can keep the URL but send info to the action and have it display its result.
I'm with Nick on this one, though I think you could just use regular views instead of having to do partials. You may need to implement them as shared views if they are not in the views corresponding to the controller (since it will only look in the associated and shared views).
public ActionResult TestRouting(string query)
{
if (query == "NewYork")
{
var model = ...somehow get "New York" model
return View("Index", model );
}
else if (query == "name-of-business")
{
var model = ...get "nameofbusiness" model
return View("Details", model );
}
else
{
return View("TestTabs");
}
}
Each view would then take a particular instance of the model and render it's contents using the model. The URL will not change.
Anytime that you use a RedirectResult, you will actually be sending an HTTP redirect to the browser and that will force a URL change.
Im not sure if you tried this way or if this way has any drawbacks..
Add a global.asax file to your project. In that add the following method:
void Application_BeginRequest(object sender, EventArgs e)
{
// Handles all incoming requests
string strURLrequested = Context.Request.Url.ToString();
GetURLToRedirect objUrlToRedirect = new GetURLToRedirect(strURLrequested);
Context.RewritePath(objUrlToRedirect.RedirectURL);
}
GetURLToRedirect can be a class that has the logic to find the actual URL based on the URL typed in. The [RedirectURL] property will be set with the url to redirect to beneath the sheets.
Hope that helps...
You can change your controller like this:
public ActionResult TestRouting(string query)
{
string controller,action;
if (query == "NewYork")
{
controller = "Availability";
action = "Index";
}
else
{
controller = "Test";
action = "TestTabs";
}
ViewBag.controller = controller;
ViewBag.action = action;
return View();
}
Then you can use these ViewBags in your view like this:
#{
Layout = null;
Html.RenderAction(ViewBag.action, ViewBag.controller);
}
That's it. And you can improve this example with use a class and some functions.
Are you saying you want to go to "www.mysite.com/NewYork" and then "really" go "somewhere else" but leave the url alone? Perhaps what you would want to do then is use partial views to implement this? That way, your base page would be what gets routed to, and then inside of that page you do your condition testing to bring up different partial views? I've done that in my application for viewing either a read-only version of a grid or an editable grid. It worked very nicely.
I'm not sure what you can do about the redirect to the .aspx page, but you should be able to replace the RedirectToAction(...)s with something like this:
public ActionResult TestRouting(string query)
{
if (query == "NewYork")
{
var controller = new AvailabilityController();
return controller.Index();
}
else if (query == "name-of-business")
return Redirect("nameofbusines.aspx?id=2731"); <--------- not sure
else
{
var controller = new TestController();
return controller.TestTabs();
}
}
How can I return the result of a different action or move the user to a different action if there is an error in my ModelState without losing my ModelState information?
The scenario is; Delete action accepts a POST from a DELETE form rendered by my Index Action/View. If there is an error in the Delete I want to move the user back to the Index Action/View and show the errors that are stored by the Delete action in the ViewData.ModelState. How can this be done in ASP.NET MVC?
[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Delete)]
public ActionResult Delete([ModelBinder(typeof(RdfUriBinder))] RdfUri graphUri)
{
if (!ModelState.IsValid)
return Index(); //this needs to be replaced with something that works :)
return RedirectToAction("Index");
}
Store your view data in TempData and retrieve it from there in your Index action, if it exists.
...
if (!ModelState.IsValid)
TempData["ViewData"] = ViewData;
RedirectToAction( "Index" );
}
public ActionResult Index()
{
if (TempData["ViewData"] != null)
{
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
...
}
[EDIT] I checked the on-line source for MVC and it appears that the ViewData in the Controller is settable, so it is probably easiest just to transfer all of the ViewData, including the ModelState, to the Index action.
Use Action Filters (PRG pattern) (as easy as using attributes)
Mentioned here and here.
Please note that tvanfosson's solution will not always work, though in most cases it should be just fine.
The problem with that particular solution is that if you already have any ViewData or ModelState you end up overwriting it all with the previous request's state. For example, the new request might have some model state errors related to invalid parameters being passed to the action, but those would end up being hidden because they are overwritten.
Another situation where it might not work as expected is if you had an Action Filter that initialized some ViewData or ModelState errors. Again, they would be overwritten by that code.
We're looking at some solutions for ASP.NET MVC that would allow you to more easily merge the state from the two requests, so stay tuned for that.
Thanks,
Eilon
In case this is useful to anyone I used #bob 's recommended solution using PRG:
see item 13 -> link.
I had the additional issue of messages being passed in the VeiwBag to the View being written and checked / loaded manually from TempData in the controller actions when doing a RedirectToAction("Action"). In an attempt to simplify (and also make it maintainable) I slightly extended this approach to check and store/load other data as well. My action methods looked something like:
[AcceptVerbs(HttpVerbs.Post)]
[ExportModelStateToTempData]
public ActionResult ChangePassword(ProfileViewModel pVM) {
bool result = MyChangePasswordCode(pVM.ChangePasswordViewModel);
if (result) {
ViewBag.Message = "Password change success";
else {
ModelState.AddModelError("ChangePassword", "Some password error");
}
return RedirectToAction("Index");
}
And my Index Action:
[ImportModelStateFromTempData]
public ActionResult Index() {
ProfileViewModel pVM = new ProfileViewModel { //setup }
return View(pVM);
}
The code in the Action Filters:
// Following best practices as listed here for storing / restoring model data:
// http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute {
protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}
:
public class ExportModelStateToTempData : ModelStateTempDataTransfer {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
//Only export when ModelState is not valid
if (!filterContext.Controller.ViewData.ModelState.IsValid) {
//Export if we are redirecting
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) {
filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
}
}
// Added to pull message from ViewBag
if (!string.IsNullOrEmpty(filterContext.Controller.ViewBag.Message)) {
filterContext.Controller.TempData["Message"] = filterContext.Controller.ViewBag.Message;
}
base.OnActionExecuted(filterContext);
}
}
:
public class ImportModelStateFromTempData : ModelStateTempDataTransfer {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;
if (modelState != null) {
//Only Import if we are viewing
if (filterContext.Result is ViewResult) {
filterContext.Controller.ViewData.ModelState.Merge(modelState);
} else {
//Otherwise remove it.
filterContext.Controller.TempData.Remove(Key);
}
}
// Restore Viewbag message
if (!string.IsNullOrEmpty((string)filterContext.Controller.TempData["Message"])) {
filterContext.Controller.ViewBag.Message = filterContext.Controller.TempData["Message"];
}
base.OnActionExecuted(filterContext);
}
}
I realize my changes here are a pretty obvious extension of what was already being done with the ModelState by the code # the link provided by #bob - but I had to stumble on this thread before I even thought of handling it in this way.
Please don't skewer me for this answer. It is a legitimate suggestion.
Use AJAX
The code for managing ModelState is complicated and (probably?) indicative of other problems in your code.
You can pretty easily roll your own AJAX javascript code. Here is a script I use:
https://gist.github.com/jesslilly/5f646ef29367ad2b0228e1fa76d6bdcc#file-ajaxform
(function ($) {
$(function () {
// For forms marked with data-ajax="#container",
// on submit,
// post the form data via AJAX
// and if #container is specified, replace the #container with the response.
var postAjaxForm = function (event) {
event.preventDefault(); // Prevent the actual submit of the form.
var $this = $(this);
var containerId = $this.attr("data-ajax");
var $container = $(containerId);
var url = $this.attr('action');
console.log("Post ajax form to " + url + " and replace html in " + containerId);
$.ajax({
type: "POST",
url: url,
data: $this.serialize()
})
.done(function (result) {
if ($container) {
$container.html(result);
// re-apply this event since it would have been lost by the form getting recreated above.
var $newForm = $container.find("[data-ajax]");
$newForm.submit(postAjaxForm);
$newForm.trigger("data-ajax-done");
}
})
.fail(function (error) {
alert(error);
});
};
$("[data-ajax]").submit(postAjaxForm);
});
})(jQuery);
Maybe try
return View("Index");
instead of
return Index();