Efficiently passing large number of parameters to ASP.NET MVC Controller Action - c#

I'm trying to determine a more efficient way to pass a large number of parameters to my controller action. I tried to look at similar questions, but they didn't seem to offer any real explanation past what I have already implemented.
As an example, I have a simple generated CRUD program that implements the PagedList package. This CRUD program needs to have multiple filters (10+). Previously, I have been passing the parameters through the URL.
Simple example:
// GET: Action/rows
[HttpGet]
public async Task<ActionResult> Index(int? page, string currentrowId, string rowId)
{
if (rowId != null)
{
page = 1;
}
else
{
rowId = currentRowId;
}
var data = from s in db.tblrows
where s.rowId.ToString().Contains(rowId)
select s;
int pageSize = 10;
int pageNumber = (page ?? 1);
ViewBag.Page = page;
ViewBag.currentrowId = rowId;
return View(await data.ToPagedListAsync(pageNumber, pageSize));
}
Then, in my view, I maintain my parameters by passing them through the URLs in each CRUD view. For example, in my index view I can open an item in the edit view using the following:
#Html.ActionLink("Edit", "Edit", new { id = item.rowId, page = ViewBag.Page, currentrowId = ViewBag.currentrowId }, new { #class = "btn btn-success btn-sm" })
In the edit view, I have similar code that maintains the current parameter so that when the user returns to the CRUD interface, their parameters are intact.
This way works fine for a few parameters, but it seems extremely tedious for many parameters. I considered creating a model for my search parameters and passing it as part of my ViewModel, but this didn't seem very efficient either when considering what that would require.
Any documentation or suggestions on a better way would be appreciated.
EDIT:
Since this is MVC and I am using a GET action method, I cannot pass an object to the method.

You can pass objects to MVC actions using HttpGet....here is an example from live code we have in our solution....I changed the objects and removed our implementation, but it is definitely possible. The [FromUri] is what tells the model binder to work with complex objects in get requests.
[HttpGet]
[Route("orderitems")]
public DataResponse<List<ItemDTO>> GetItems([FromUri]SearchObject search)
{
// Do stuff
}

You can pass an object as parameter. It's a technique used when you have a large number of parameters.
See more details here:
https://www.includehelp.com/dot-net/how-to-pass-object-as-argument-into-method-in-c-sharp.aspx

Related

ASP.NET MVC 5 Keeping old input between requests

I need feature that is something similar to Laravel's old input helper but in MVC 5.
https://laravel.com/docs/5.6/requests#old-input
If validation fails, I need to reload all my model data as it was in the previous request except those inputs where user entered something wrong.
The problem is that my form has many disabled inputs and fields that program is fetching within [HttpGet] method, and they're getting lost during submission. So I need to store them in session.
The code below seems to work but is there any more efficient and beautiful way to do so with a less amount of code within each controller?
[HttpGet]
[Route(#"TaskManagement/Edit/{guid}")]
public async Task<ActionResult> Edit(Guid guid)
{
var model = new EditTaskViewModel();
model.Guid = guid;
await model.GetTaskFromRemoteService(new UserInfo(User));
ControllerHelpers.DisplayAlerts(model, this);
TempData["OldModel"] = model;
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route(#"TaskManagement/Edit/{guid}")]
public async Task<ActionResult> Edit(EditTaskViewModel model, Guid guid, string submit)
{
model.Guid = guid;
if (ModelState.IsValid) {
await model.UpdateTaskInRemoteService(new UserInfo(User), submit);
ControllerHelpers.DisplayAlerts(model, this, "Task successfully updated");
if (model.ErrorCode == null)
return RedirectToAction("Edit", new { guid = model.Guid });
return RedirectToAction("Index");
}
if (TempData["OldModel"] != null) {
model = (EditTaskViewModel)TempData["OldModel"];
}
return View(model);
}
Using session state (including TempData) like this may break when you have multiple copies of the page open. You can work around this by generating a unique ID for the session key and storing it in a hidden field.
However, I would try to avoid using session altogether.
A simple approach is to use hidden fields to store the values that aren't sent to the server because they are in disabled fields.
A more robust approach is a separate class (or at least a private method) that knows how to setup your model for the first time and in transition (e.g. failed server validation). I call these classes "composers" and I describe the approach here.
Pseudocode for how an action method with a composer might look:
if( ModelState.IsValid ){
return Redirect();
}
var rebuiltModel = _composer.ComposeEdit( incomingModel );
return View( rebuiltModel );
I think the answer was quite simple. The shortest and easiest way is to populate the object from the database\remote service once more.
The fields that user entered whether they're valid or not will stay as they were before. The rest of them will load once again.

No need for separate controllers or actions for each view solution - understanding the code

There are two things I can't grasp in this article. It explains the way to use ASP.NET MVC without the need for separate controllers or actions for each view.
1) In DispatchRequest method:
private void DispatchRequest(IControllerFactory controllerFactory, string controller, string action)
{
var route = GetRoute(controller, action);
_requestContext.RouteData.Values["x-action"] = action;
_requestContext.RouteData.Values["x-controller"] = controller;
if (route != null)
{
_requestContext.RouteData.Values["controller"] = route.Controller;
_requestContext.RouteData.Values["action"] = route.Action;
if (route.Area != string.Empty)
{
_requestContext.RouteData.DataTokens["area"] = route.Area;
}
controller = route.Controller;
the action and controller strings are stored under "x-action" and "x-controller" keys. A few lines below the controller and action are stored under "controller" and "action" keys.
Both pairs (controller and action) are strings, aren't these pairs the same? It looks to me like they are. Why duplicate the data unnecessarily?
2) In the controller, ControllerLessController :
public virtual ActionResult Index()
{
var action = RouteData.Values["x-action"].ToString();
var controller = RouteData.Values["x-controller"].ToString();
RouteData.Values["action"] = action;
RouteData.Values["controller"] = controller;
if (RouteData.Values["area"] != null)
{
RouteData.DataTokens["area"] = RouteData.Values["area"].ToString();
}
return View(action);
}
}
Notice the first two lines in the body. Why invoke toString on string objects? Moreover, why somebody decided to store them in action and controller variables and overwrite the data under "action" and "controller" keys (line 3,4)?
Both pairs (controller and action) are strings, aren't these pairs the same? It looks to me like they are. Why duplicate the data
unnecessarily?
No. The intent is to store the original request values in "x-action", "x-controller" and then overwrite "action", "controller" as necessary while still having access to the original values at a later stage in processing. "x-action", "x-controller" are simply being used as temp vars. They are being stored in RouteData because once the dispatch method completes any local vars will go out of scope.
Notice the first two lines in the body. Why invoke toString on string objects?
RouteData.Values returns an object via a string indexer hence the ToString. i.e.
RouteData.Values["MyValue"] returns an object not a string.
Moreover, why somebody decided to store them in action and controller
variables and overwrite the data under "action" and "controller" keys
(line 3,4)?
This goes back to the TempData idea in 1. An action request comes in. Normally in MVC that would translate to a controller with a view but here in this controller-less example controller-less actions need to be mapped to the controllerless-handler.
So in DispatchRequest these are overridden to point at the controllerless handler class ControllerLessController : Controller.
Note this is happening prior to controller selection.
Then MVC processes the request in the normal fashion but because of the switching around in Dispatch MVC doesn't go looking for the originally requested controller (since there wasn't one) instead it uses the injected controller-less controller:
_requestContext.RouteData.Values["action"] = _configuration.DefaultAction;
controller = _configuration.DefaultController;
So then the normal request processing carries on and lands within the controllerless controller. At that point we need to reach back and find which view was originally requested.
public virtual ActionResult Index()
{
var action = RouteData.Values["x-action"].ToString();
var controller = RouteData.Values["x-controller"].ToString();
RouteData.Values["action"] = action;
RouteData.Values["controller"] = controller;
if (RouteData.Values["area"] != null)
{
RouteData.DataTokens["area"] = RouteData.Values["area"].ToString();
}
return View(action);
}
This info was stored in the "x-action" routes values so they pull that out and return a view for the original request:
return View(action);
where
var action = RouteData.Values["x-action"].ToString();
Basically you just have a layer of re-direction/interception. We re-direct all actions without controllers to some handler, in this case the ControllerLessController. Before we do that we need to store the original request params in "x-action" vars. Then once in the ControllerLessController handler we pull those original values back out so we can produce a view for the original controller request.

ASP.Net MVC Refresh Page without destroying ViewModel

I want to create a multilingual webpage. To switch between languages I've got a dropdown on my page. If the change event of the dropdown gets fired the Method called "ChangeLanguage" in my Controller is called.
public ViewModels.HomeViewModel HVM { get; private set; }
// GET: Home
public ActionResult Index()
{
this.HVM = new ViewModels.HomeViewModel();
return View(this.HVM);
}
public JsonResult ChangeLanguage(int id) {
return Json(new {Success = true});
}
Now I'd like to to change my "SelectedLanguage" Property in my ViewModel (HVM) - but the Reference is null. May anyone explain why HVM is null in my ChangeLanguage Method?
After my SelectedLanguage Property is changed I'd like to reload my whole page to display it's texts in another language
e.g.
#model ViewModels.HomeViewModel
<html>
<div class="HeaderText">
Text = #{
#Model.TextToDisplay.Where(o =>
o.Language.Equals(Model.SelectedLanguage)).First()
}
</div>
Here's what I want to do in PseudoCode:
PseudoCode:
public JsonResult ChangeLanguage(int id) {
this.HVM.SelectedLanguage =
this.HVM.AvailableLanguages.Where(o =>
o.ID.Equals(id)).First();
Page.Reload();
return Json(new {Success = true});
}
May anyone explain why HVM is null in my ChangeLanguage Method?
Adhering to stateless nature of HTTP protocol, all (unless explicitly added into request header) requests (MVC method calls) loose state data associated with it. Web server treats every request a new request and creates new instances of classes right from controller itself.
In your case since it is a new request, controller has a HVM property defined but in ChangeLanguage it is not instantiated (it gets instantiated only into Index method which is not called when you invoke ChangeLanguage) hence it is null.
After my SelectedLanguage Property is changed I'd like to reload my
whole page to display it's texts in another language.
Option 1: Refresh page
Simple option to implement. Pass the language selection to server, server will return a new view with specific data. Drawback, whole page will refresh.
Option 2: Update view selectively
If option 1 is really not acceptable, then consider this option. There are multiple ways you can achieve it. Basically it involves either (a) breaking you view into partial view and update only the portion that is affect by selection or (b) bind data element with a JS object.
(a) - Not much need to be said for this.
(b) - Data binding can easily be done if you employ a JS library like KnockoutJS.
Change your methods to these methods , This trick will work for you =>pass your model to Change language from view. Also update JsonResult to ActionResult.
public ActionResult ChangeLanguage(ViewModels.HomeViewModel model,int id)
{
this.HVM.SelectedLanguage =
this.HVM.AvailableLanguages.Where(o =>
o.ID.Equals(id)).First();
return RedirectToAction("Index",model);
}
public ActionResult Index(ViewModels.HomeViewModel model)
{
if(model == null)
{
this.HVM = new ViewModels.HomeViewModel();
}
return View(this.HVM);
}

Trying to use DropDownListFor without model getting "tree may not contain dynamic operation" error

I am trying to use a DropDownListFor<> in my LayoutTemple so I don't have access to a model. So what I did was in the #{} block at the top of the page I added FFInfo.DAL.SoloClassesContext db = new FFInfo.DAL.SoloClassesContext(); which calls an instance of the DBContext with the class I want to use. I then placed the List where I wanted using
#Html.DropDownListFor(
m => m.ID,
new SelectList(db.CultureCodes, "ID", "DisplayName"));
but when I run the code I get an error for the line m => m.ID. The error given is:
An expression tree may not contain a dynamic operation
I have never used this type of dropdown and am very new to MVC. Can anyone tell me what I am doing wrong and how to fix it?
I suggest some changes in your solution:
Instead of DropDownListFor() use just DropDownList()
#Html.DropDownList("CultureCode", new SelectList(db.CultureCodes, "ID", "DisplayName"))
Instead of accessing your database data in your view... which is very out of the standard and you are coupling the Views (usually HTML) with the database... you should put the query in your controller and put the data in the ViewBag collection.
So, in your Layout, instead of the code I suggested above, you should use:
#Html.DropDownList("CultureCode", (SelectList)ViewBag.Codes, "Select one...")
In your controller you load it as follow:
ViewBag.Codes = new SelectList(db.CultureCodes, "ID", "DisplayName");
EDIT:
You can do an action filter, to load or inject the CultureCodes in the ViewBag:
public class IncludeCultureCodesAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.Controller;
// IController is not necessarily a Controller
if (controller != null)
{
var db = new YourContext();
controller.ViewBag.Codes = new SelectList(db.CultureCodes, "ID", "DisplayName"));;
}
}
}
Then, in your Controller actions... you can decorate them with [IncludeCultureCodes]. So, the action with that attribute will load the collection of codes.
But I think is a better approach to load one time the layout (on Home/Index for example) and then use partial views. This way you only reload the layout going back to home... or other full view calls.

.NET MVC 4 - Multiple "actions" on same Controller, how?

I'm pulling my hair out over this one and I'm looking for guidance before I start fudging together my own approach.
Here's what I've got:
View snippet
<td>#Html.ActionLink("More Details", "Index", new { id = product.ProductId })</td>
<td>#Html.ActionLink("Compare", "Compare", new { id = product.ProductId, compare = true })</td>
Controller snippet
public ActionResult Index(FormCollection values)
{
// Does stuff, works
}
public ActionResult Index(int productId)
{
// Does stuff, works
}
Now, here lies my problem. The Index functions are both taken now, from the POST to the form, and the "More Details" ActionLink being clicked. This works fine.
Now I want "Compare" to be functional, in which I want on the same page and will hold a list of compared products, which is fine. But how do I get that Compare functionality on the same View/Page?!
I've tried:
public ActionResult Compare(int productId)
{
}
But obviously that doesn't work as it requires a Compare.cshtml, which I don't want to happen. I want it to be modify my ViewModel and return it with newly Compared products, so I'd be able to do this from my original View:
#foreach(var products in Model.ComparedProducts)
The only way I can see me doing this is "fudging it" to have:
public ActionResult Index(int productId = 0, bool compare)
{
}
Which could become unruly with lots of functionality on the same page.
Surely there's something obvious I'm missing here?
Oh, and the AjaxLink options isn't right for me, as this is part of the site that has to work via postbacks (Progress Enhancement and all that jazz).
I think you return an ActionResult by calling return View(model), is that right? Without naming a view explicitly, the MVC resolving mechanism looks for views with the same name as the action, in your case "Compare.cshtml".
If you change your call to return View("Index", model) you will be using the Index.cshtml view regardless of the action name.
Is that what you were looking for?
An action in MVC does not require a corresponding View. It can return any view by supplying a name parameter to the View() function - see http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.view%28v=vs.98%29.aspx for details.
You can in your compare function do all the logic required and redirect back to the index action or any other that display the page as needed using RedirectToAction().

Categories