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.
Related
We're working diligently at learning Orchard, with a goal of creating not websites, but business-centric web applications that we would normally write in months using MVC, but hope to be much more efficient by using the various Parts already available.
The last mile, though, seems to be a big block -- how to tell Orchard that it should create a shape that allows the end-user to edit some data? There's a good bit on most of end-user editing at Creating a module for Orchard that stores data from the front-end but it picks up after the data's already been entered and carries it through the controller's POST operation. What I can't figure out is how to get through the initial GET operation.
To elaborate, in straight MVC I might allow the user to enter information about themselves. So I'd have /Controllers/PersonController.cs and there write a Create() function. I'd add a View in /Views/Person/Create.cshtml and simply "return View()" from the controller. In Create(Person p), an HTTPGet method, I'd do the heavy lifting to save the object.
Now in Orchard I have my PersonPart and its PersonPartDriver which, as I understand from the above article, I would write my POST method to accept the PersonPart and save the object.
class PersonController : Controller
{
private readonly IRepository<PersonPartRecord> personRepository;
public PersonController(IRepository<PersonPartRecord> _personRepository) {
personRepository = _personRepository;
}
[HttpPost]
public Create(PersonPart part) {
personRepository.Create(part.Record);
}
}
All well, but how would I get Orchard to invoke the GET Editor(PersonPart, dynamic) method to get the form up for the user to do the initial data entry?
protected override DriverResult Editor(PersonPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Person_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: "Parts/Person",
Model: part,
Prefix: Prefix));
}
Or do I write the GET Create() method in the controller? If I do that, though, I'm bypassing the entire shape creation system, no? Something itches in the back of my brain saying I should rather be doing a Display() and, in the template, just making it an editable form, but I have a Display() for the readonly view of Person ... how to make it know I want the editable view?
Hope the question makes sense, and hope that someone can assist.
Thanks.
Have a look to Orchard.CustomForms
var model = _contentManager.BuildEditor(contentItem);
return View(model);
but you'll need something like the code above. You could return ShapeResult(this,model) too
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.
I'm trying to have a View where the user can add items in a collection without having to go to a new View (the scenario is a sort of CV site where the user adds info about work experience, skills, etc, and it would seem absurd to go to a new View to add each little thing).
So I have an edit View that shows a number of text boxes for the already added items, and there's an ajax call to go to a method to fetch the collection fresh if the user adds an item.
Here are the methods in question:
public ActionResult Edit(int id)
{
Consultant consultant = _repository.GetConsultant(id);
var vm = GetViewModel(consultant);
return View(vm);
}
private DetailsViewModel GetViewModel(Consultant consultant)
{
return new DetailsViewModel
{
Programs = consultant.Programs.ToList(),
Consultant = consultant
};
}
public ActionResult NewProgram(int id)
{
//TODO: ordering is rather strange, because the entitycollection adds at the beginning rather than the end...
Consultant consultant = _repository.GetConsultant(id);
consultant.Programs.Add(new Program());
_repository.Save();
var vm = GetViewModel(consultant);
return PartialView("ProgramList", vm);
}
Now to the question: When the NewProgram method is called, it adds a new program to the Consultant object and creates a new ViewModel to send back, but it adds the new program to the start of the EntityCollection, not at the end. But then when you post the entire form, and you open the Edit View again, the list will have placed the new added program at the end. This is very strange. The user will think he/she adds an item at the start of the list, but if they go back to the page again, they will find the new item at the end.
Why does it do this, and is there any way I can make NewProgram() have the new program added at the end directly?
And if anyone is thinking "he should be using a ViewModel" with DTOs instead of working directly with EF objects, well, I've been down that road for quite a while now ( Entity Framework and MVC 3: The relationship could not be changed because one or more of the foreign-key properties is non-nullable ), and so far no one has shown me explicitly how to achieve this and still be able to both add and remove items in the same View. There is either a problem with maintaining the indexes of the collections or the Entity Framework won't let me save... And the code became a nightmare.
This way I at least have understandable code, and the only thing is I need to have this adding done in the "normal" order, i.e. add at the end of the collection...
Any ideas?
BTW:
This works, but it seems very unnecessary to first have to add the new program to the Consultant object, create the ViewModel without the new program, and then add it to the ViewModel separately...
public ActionResult NewProgram(int id)
{
//TODO: ordering is rather strange, because the entitycollection adds at the beginning rather than the end...
Consultant consultant = _repository.GetConsultant(id);
var vm = GetViewModel(consultant);
var program = new Program();
consultant.Programs.Add(program);
_repository.Save();
vm.Programs.Add(program);
return PartialView("ProgramList", vm);
}
According to http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx , your navigation property Programs is overridden to invoke some kind of DoLazyLoad() method. Since the property instance itself isn't necessarly changed, DoLazyLoad() might actually by asynchronous, which could account for the behavior you're noticing.
Since you are evaluating the list anyhow, you could call ToList() before adding the new program. It would require you to only change the line a bit:
consultant.Programs.ToList().Add(new Program());
If this doesn't work, try:
consultant.Programs.ToList();
consultant.Programs.Add(new Program());
This actually doesn't work well with my "asynchronous" theory, but might help you out.
After a pair programming session, an interesting question came up which I think I know the answer for.
Question: Is there any other desired way in ASP.NET MVC to retain 'state' other than writing to database or a text file?
I'm going to define state here to mean that we have a collection of person objects, we create a new one, and go to another page, and expect to see the newly created person. (so no Ajax)
My thoughts are we don't want any kung-fu ViewState or other mechanisms, this framework is about going back to a stateless web.
What about user session? There are plenty of valid use cases to store things in session. And what about a distributed caching system like memcached? You also seem to leave out the query string - which is an excellent state saver (?page=2). To me those seem like other desirable methods to save state across requests...?
My thoughts are we don't want any kung-fu ViewState or other mechanisms, this framework is about going back to a stateless web.
The example you provided is pretty easy to do without any sort of "view state kung fu" using capabilities that are already in MVC. "User adds a person and sees that on the next screen." Let me code up a simple PersonController that does exactly what you want:
public ActionResult Add()
{
return View(new Person());
}
[HttpPost]
public ActionResult Add(PersonViewModel myNewPersonViewModel)
{
//validate, user entered everything correctly
if(!ModelState.IsValid)
return View();
//map model to my database/entity/domain object
var myNewPerson = new Person()
{
FirstName = myNewPersonViewModel.FirstName,
LastName = myNewPersonViewModel.LastName
}
// 1. maintains person state, sends the user to the next view in the chain
// using same action
if(MyDataLayer.Save(myNewPerson))
{
var persons = MyDataLayer.GetPersons();
persons.Add(myNewPersion);
return View("PersonGrid", persons);
}
//2. pass along the unique id of person to a different action or controller
//yes, another database call, but probably not a big deal
if(MyDataLayer.Save(myNewPerson))
return RedirecToAction("PersonGrid", ...etc pass the int as route value);
return View("PersonSaveError", myNewPersonViewModel);
}
Now, what I'm sensing is that you want person on yet another page after PersonSaveSuccess or something else. In that case, you probably want to use TempData[""] which is a single serving session and only saves state from one request to another or manage the traditional Session[""] yourself somehow.
What is confusing to me is you're probably going to the db to get all your persons anyway. If you save a person it should be in your persons collection in the next call to your GetPersons(). If you're not using Ajax, than what state are you trying to persist?
ASP.NET MVC offers a cleaner way of working with session storage using model binding. You can write a custom model binder that can supply instances from session to your action methods. Look it up.
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")?