How to persist a data set between MVC calls - asp.net-mvc

The following setup kind of works, but it's not the proper use of TempData, and it falls down after the first use. I suspect I'm supposed to be using ViewData, but I'm looking for some guidance to do it correctly.
In my Home controller, I have an Index action that shows which members need to have letters created:
Function Index() As ActionResult
Dim members = GetMembersFromSomeLongRunningQuery()
TempData("members") = members
Return View(members)
End Function
On the Index view, I've got a link to create the letters:
<%=Html.ActionLink("Create Letters", "CreateLetters", "Home")%>
Back in the Home controller, I've got a CreateLetters function to handle this:
Function CreateLetters() As ActionResult
Dim members = TempData("members")
For Each member In members
'[modify member data regarding the letter]
Next
TempData("members") = members
Return RedirectToAction("Letter", "Home")
End Function
Finally, there is a Letter view which creates the actual letters. It's ContentType is "application/msword", so the data actually comes up in Word instead of the browser.
Like I said, this works great the first time through. However, since the letters come up in Word, when the user comes back to the browser, they are still sitting on the screen with the "Create Letters" link. If they try to hit it again (i.e. paper jam after the first attempt), they get an error in CreateLetters because TempData("members") is now empty. I know this is how TempData works (single shot requests), which is why I'm trying to move away from it.
How would this be refactored to use ViewData? Alternatively, how could I simultaneously display the Letter page (i.e. Word document) and redirect the app back to the Index?

I would simply retrieve the object again on the other view but why not cache the object when it is first retrieved
Function Index() As ActionResult
Dim members = GetMembersFromSomeLongRunningQuery()
// Pseudo code (am a C# man sorry)
Cache.Remove("MembersItem")
Cache.Add(members,"MembersItem", CacheExpiry....)
TempData("members") = members
Return View(members)
End Function
Function CreateLetters() As ActionResult
Dim members = Cache.Get("MembersItem")
For Each member In members
'[modify member data regarding the letter]
Next
TempData("members") = members
Return RedirectToAction("Letter", "Home")
End Function
You should also consider caching the results of the query for a specified time or make it dependent on an update. You could also use the session object if it is user specific data.

TempData is designed for keeping data across a redirect and ViewData is designed for passing data to the view. Neither of these are really suitable to what you are trying to achieve although TempData can be made to work in this way by creating an extension method that you can call in the Letter action ( as well as replacing the TempData("members") = members line in CreateLetters )
using System.Web.Mvc;
namespace GoFetchV2.ExtensionMethods
{
public static class TempDataExtensions
{
public static void Keep( this TempDataDictionary tempData, string key )
{
var value = tempData[ key ];
tempData[ key ] = value;
}
}
}
Used like ...
Function CreateLetters() As ActionResult
Dim members = TempData("members")
' Do something with members but want to keep it for the next action '
TempData.Keep("members")
Return RedirectToAction("Letter", "Home")
End Function
Function Letter() As ActionResult
Dim members = TempData("members")
' Do something with members but want to keep it for the next action '
TempData.Keep("members")
' Return the msword binary data '
End Function

Could you just stuff this in an application variable? Application("members")?

Related

Remote validation asp.net mvc

I am new in mvc so forgive me if the question is stupid but I want to do the best I can. So, my situation is that:
I have created a model and decorated like
Partial Public Class App_Modules
<Required>
<Remote("CheckForDuplicate", "Validation")>
<Display(Name:="Code")>
Public Property code As String
<Required>
<Display(Name:="Description")>
Public Property name As String
End Class
As you can see, the code column must be remote validated.
In my ValidationController I have the code
Public Function CheckForDuplicate(code As String) As JsonResult
Dim data = db.App_Modules.Where(Function(p) p.code.Equals(code, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault()
If data IsNot Nothing Then
Return Json("This code already exists",JsonRequestBehavior.AllowGet)
Else
Return Json(True, JsonRequestBehavior.AllowGet)
End If
End Function
End Class
Everything works fine! Now I want to do the same for another model with the same field "code". Is there any way to pass the model name to the function so instead of the line
Dim data = db.**App_Modules**.Where(Function(p) p.code.Equals(code, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault()
I could have something like
Dim data = db.**TABLENAME**.Where(Function(p) p.code.Equals(code, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault()
So the function would be generic and can be called from other models too?
I am not sure of the syntax in VB but you could modify your CheckForDuplicate function to accept a generic parameter that represents your Model class and pass that to the Set function of your DBContext. you will also need to define an interface for your Model that contains the Code property. Sample code in c# is as follows.
public JsonResult CheckForDuplicate<T>(string code) where T : IModelWithCode
{
var data = db.Set<T>().Where(t => t.Code.Equals(code));
....
}
public interface IModelWithCode
{
string Code { get; set; }
}
Hopefully that will get you started in the right direction.
I don't think this can be done or at least easily.
I would stick with the simple here: create a Select Case and check in the tables depending on the parameter passed (model name).
Dim exist = false;
Select Case myModel
Case "Model1"
exist = db.Model1Table.Where(Function(p) p.code.Equals(code, StringComparison.CurrentCultureIgnoreCase)).Any()
Case "Model2"
exist = db.Model2Table.Where(Function(p) p.code.Equals(code, StringComparison.CurrentCultureIgnoreCase)).Any()
End Select
If each table has different layout or you have to do some other checks... you are free to do the special thing in each case.
UPDATE:
Here you can see an article showing how to pass other fields to the validator action.
You should create a Hidden Field to hold the Model name.
http://www.codeproject.com/Articles/674288/Remote-Validation-in-MVC-Simple-Way-to-Pass-the-F
Other resource: MVC Remote Attribute Additional Fields

ASP MVC4: A issue when manipulating variable value in controller

Thanks everyone's effort of helping me out, basically I'm facing a problem in controller below, just make it simple and easy:
Controller C{
public list<model> a;
//used in action A, if it's a searched list, then don't initialize;
public bool searched = false;
public ActionResult A(){
if(searched){
ViewBag.a = a;
}else
//initial the list
a = db.model.where();
.....
return view()
}
//return a result list when people search by keywords
public ActionResult B(string str){
a = db.model.where(d=> d.id = str);
search = true;
}
}
But, it turned out that the value of both a and researched never changed after B called
Did I miss some critical knowledge in .NET MVC?
Any related articles are very welcomed
Thanks
You should use TempData to keep your value after redirect. It is exactly TempData was designed for.
In your example it will be:
Controller C{
public ActionResult A(){
TempData["str"] = "this is A";
return RedirectToActionB()
}
public ActionResult B(){
TempData["str"] += "This is B";
return View()
}
}
I'm guessing you're asking because it's not giving the result you expect, rather than because you want someone to try it for you. The easy answer (assuming you meant "On the B's View Page") is "This is B".
A RedirectToAction will send a redirection to the browser, which initiates a new request to the server. Unfortunately a ViewBag's life is only for a single request, so it will no longer be populated when action B runs.
You will need to find another way to pass the data; for example, in a cookie or within the Session object.
Instead of:
return RedirectToActionB()
Try:
return B();
You'll save a redundant Http request as well.
In asp.net mvc controller doesn't keep it's fields/properties during requests: you'll have new instance of C on each web request. This means that you cannot expect a and searched fields to keep their state after action B completion.
You may use ViewBag(ViewData) to pass values between controller and view during request.
You may use TempData to store values during 2 requests, it's usually being used in PRG pattern for example.
You may use Session to keep your values while client's session is alive.
You may pass values between requests using hidden inputs on your page, query parameters, or less obvious things like HttpHeaders and others.
If you want to pass values between actions A and B, you may use TempData or Session is collection isn't big (it'll be stored in encrypted cookie which size cannot exceed 4k as far as I remember).
There is an another option which may be useful. You may store your collection in client's localstorage if it's ok in your case.

.NET MVC: A Q when changing the value in controller

Thanks everyone's effort of helping me out, basically I'm facing a problem in controller below, just make it simple and easy:
Controller C{
public list<model> a;
//used in action A, if it's a searched list, then don't initialize;
public bool searched = false;
public ActionResult A(){
if(searched){
ViewBag.a = a;
}else
//initial the list
a = db.model.where();
.....
return view()
}
//return a result list when people search by keywords
public ActionResult B(string str){
a = db.model.where(d=> d.id = str);
search = true;
}
}
But, it turned out that the value of both a and researched never changed after B called
Did I miss some critical knowledge in .NET MVC?
Any related articles are very welcomed
Thanks
You're probably thinking the state will be preserved between two web requests. But when the web request ends, the entire controller is destroyed and the set information is lost unless it is stored in a persistent data store like Session or a database.
If I understand your code correctly, if you refactor your code slightly, you can probably achieve your searching functionality under one action, and you wouldn't need to store the data persistently.
Controller C will be recreated on each request, so even though the values are updated after B is called, the next request for A will require the creation of controller C so a an search will be reinstantiated.
You might want to make the local variables static.

Pass a big object to an action on a controller from a view

I want to pass a big object to a controller's action from a view. Like so:
View
<div>#Html.ActionLink("Send us an email", "Index",
"Email", new { o = #Model.Exception }, null)</div>
Controller
public class EmailController : Controller
{
[AllowAnonymous]
public ActionResult Index(object o)
{
new BaseServices.Emailer().SendEmail(o);
return View();
}
}
The thing is: the object being passed is so large that I guess that MVC is unable to make an argument out of that and add it to the route table/dictionary. So, my email controller's Index action is never called. The code bombs off somewhere in between.
No, you can't do this. ASP.NET MVC is not some magic. It relies on standard HTTP and HTML. And as you know in HTTP when you are using a GET request, there's no notion of .NET objects. You cannot ask how to pass an object in a web application because this is not defined.
There's a notion of query string parameters. So that's what you can pass => simple query string parameters:
#Html.ActionLink(
"Send us an email",
"Index",
"Email",
new { id = Model.Exception.Id, text = Model.Exception.Text },
null
)
Where the magic comes is that ASP.NET MVC will now use the 2 simple query string parameters (id and text) to map them to the corresponding properties of your view model inside the controller action.
But of course for this to work ASP.NET MVC needs to know the type of the model. You cannot just use object because this type doesn't have id nor text properties.
So:
public ActionResult Index(MyViewModel o)
Now but what about sending complex types? Well, the question that you have to ask to yourself is why on the first place this type was passed to the view? Was it because tfhe user was supposed to edit some of its properties? Is so then you should use an HTML form containing input fields allowing the user to edit them.
But since you have stuck this object into an anchor then what's the point? The server could fetch this object from wherever it fetched it in the first place. So all you need is to pass a simple id to the server:
#Html.ActionLink(
"Send us an email",
"Index",
"Email",
new { id = Model.Exception.Id },
null
)
and have the controller action take this id as parameter:
public ActionResult Index(int id)
Alright now you know the id => therefore you could retrieve the corresponding entity from wherever this entity is persisted.
Now some people might suggest you storing the object into the session before rendering the view and then retrieving this object from the session. Personally I am not a big fan of the session as it introduces state into the application. This means that you can never invoke the second action without first invoking the first action. This also means that you cannot bookmark the second action into the browser favorites. This also means that if you are running your application in a web-farm you can no longer store the session in-memory => you will have to use an out-of-process storage for this session. Sessions are way too much of a hassle.
You can't really pass it in the view.
Instead, consider storing the exception in TempData in the controller that renders the view....
public ActionResult DisplayErrorAndOptionToEmailIt()
{
TempData["LastError"] = m.Exception;
return View();
}
and then when the request comes in retrieve it from temp data and email it
public ActionResult SendTheEmail()
{
var e = TempData["LastError"] as Exception;
if (e != null)
{
EmailHelper.SendExceptionEmail(e);
}
return View();
}
On a side note, it's not the best practice to store complete objects. If possible, store only what you need:
public ActionResult DisplayErrorAndOptionToEmailIt()
{
TempData["LastError"] = m.Exception.Message;
return View();
}

Returning data with viewmodel in POST request

I have a view model like such:
public class MyViewModel
{
public string Name { get; set; }
public List<Purchases> Purchases { get; set; }
}
This viewmodel is sent to a view that allows the user to edit the name property. The Purchases property is used only to create a dropdown box for it:
<%: Html.DropDownListFor(t => t.Name, new SelectList(Model.Purchases, "Value", "Text")) %></p>
This works fine.
However, when I perform server-side validation and then return to the View, I'm getting an object null reference error because the Purchases property is now set to null. I'm guessing this is because when the form is submitted because the Purchases property isn't bound to any editable control, it isn't being passed back with the viewmodel.
How can I prevent this happening? I want to send back the List to be send back with the Post request always.
You don't need to send back the list. If validation fails then simply rebuild the view model from scratch. One of the main selling points of MVC is how well it works in a stateless environment. Web Forms used ViewState to do this kind of thing, I don't think you want to replicate this kind of functionality though.
I like to have two overloaded Action methods for this (both with the same name but different method signatures). One with an [HttpGet()] attribute and the other with an [HttpPost()]. If your model is found to be invalid on the POST then simply return the "GET" method (NOTE: you'll need to to pass in any parameters required to rebuild the view).
When I say return, I mean:
return MyGetAction();
and not a Redirect to the GET action.
If the model is valid then you could/should perform a RedirectToAction() to a GET Action (this means if the user hits the refresh button it won't submit the form again, this is called the Post/Redirect/Get (PRG) pattern)
You'd have to create a hidden input for each of the elements in the list in addition to the select list. Having said, that I think caching the results of the query on the server is a better way to handle repopulating the list if you don't want to perform the query again. There's no sense in sending the data back across the wire if the server can just hang on to it. Personally, I wouldn't even bother with the caching unless it proved to be a performance bottleneck. Just populate the model selection list from the DB.
<% for (int i = 0; i < Model.Purchases.Length; ++i) { %>
<%: Html.Hidden( string.Format( "Purchases[{0}]", i ), Model.Purchases[i] ) %>
<% } %>
Lee Gunn is spot on. To make the answer a little more concrete, it is very common to re-build non-scalar values in the event ModelState is not valid. This can be done by simply returning the [HttpGet] version in your Controller. However, you could simply re-build the purchases collection manually. It's up to you.
[HttpGet]
public ActionResult MyView(string name)
{
//get entity and build up a view model
var entity = _myDb.GetEntity(name);
MyViewModel vm = AutoMapper.Map<Entity, MyViewModel>(entity);
return vm;
}
[HttpPost]
public ActionResult MyView(MyViewModel vm)
{
If(!ModelState.IsValid)
{
//here is one way of doing it
return MyView("");
//OR
vm.Purchases = GetSomePurchasesBro();
return View(vm);
}
//continue on persisting and doing the Post Redirect Get
}
P.S.
Calling
return MyView("");
can be replaced with
return MyView(vm.Name);
Both will do the same thing (provided you're using the Html Helper Extensions, i.e. Html.TextBoxFor(x=>x.Name))
Defaut model binding looks in the ModelState for attemptedValues when rendering Html. This is described here by Steve Sanderson.

Resources