I'm getting started with FubuMVC and I have a simple Customer -> Order relationship I'm trying to display using nested partials. My domain objects are as follows:
public class Customer
{
private readonly IList<Order> orders = new List<Order>();
public string Name { get; set; }
public IEnumerable<Order> Orders
{
get { return orders; }
}
public void AddOrder(Order order)
{
orders.Add(order);
}
}
public class Order
{
public string Reference { get; set; }
}
I have the following controller classes:
public class CustomersController
{
public IndexViewModel Index(IndexInputModel inputModel)
{
var customer1 = new Customer { Name = "John Smith" };
customer1.AddOrder(new Order { Reference = "ABC123" });
return new IndexViewModel { Customers = new[] { customer1 } };
}
}
public class IndexInputModel
{
}
public class IndexViewModel
{
public IEnumerable<Customer> Customers { get; set; }
}
public class IndexView : FubuPage<IndexViewModel>
{
}
public class CustomerPartial : FubuControl<Customer>
{
}
public class OrderPartial : FubuControl<Order>
{
}
IndexView.aspx: (standard html stuff trimmed)
<div>
<%= this.PartialForEach(x => x.Customers).Using<CustomerPartial>() %>
</div>
CustomerPartial.ascx:
<%# Control Language="C#" Inherits="FubuDemo.Controllers.Customers.CustomerPartial" %>
<div>
Customer
Name: <%= this.DisplayFor(x => x.Name) %> <br />
Orders: (<%= Model.Orders.Count() %>) <br />
<%= this.PartialForEach(x => x.Orders).Using<OrderPartial>() %>
</div>
OrderPartial.ascx:
<%# Control Language="C#" Inherits="FubuDemo.Controllers.Customers.OrderPartial" %>
<div>
Order <br />
Ref: <%= this.DisplayFor(x => x.Reference) %>
</div>
When I view Customers/Index, I see the following:
Customers
Customer Name: John Smith
Orders: (1)
It seems that in CustomerPartial.ascx, doing Model.Orders.Count() correctly picks up that 1 order exists. However PartialForEach(x => x.Orders) does not, as nothing is rendered for the order. If I set a breakpoint on the Order constructor, I see that it initially gets called by the Index method on CustomersController, but then it gets called by FubuMVC.Core.Models.StandardModelBinder.Bind, so it is getting re-instantiated by FubuMVC and losing the content of the Orders collection.
This isn't quite what I'd expect, I would think that PartialForEach would just pass the domain object directly into the partial. Am I missing the point somewhere? What is the 'correct' way to achieve this kind of result in Fubu?
Update: In case it helps, this is the top few lines of the stacktrace the first time the Order constructor gets hit:
at FubuDemo.Customer..ctor()
at FubuDemo.Controllers.Customers.CustomersController.Index(IndexInputModel inputModel)
at lambda_method(ExecutionScope , CustomersController , IndexInputModel )
at FubuMVC.Core.Behaviors.OneInOneOutActionInvoker`3.performInvoke()
And the second time:
at FubuDemo.Customer..ctor()
at System.RuntimeType.CreateInstanceImpl(Boolean publicOnly, Boolean skipVisibilityChecks, Boolean fillCache)
at System.Activator.CreateInstance(Type type, Boolean nonPublic)
at System.Activator.CreateInstance(Type type)
at FubuMVC.Core.Models.StandardModelBinder.Bind(Type type, IBindingContext context)
at FubuMVC.Core.Runtime.ObjectResolver.BindModel(Type type, IBindingContext context)
at FubuMVC.Core.Runtime.ObjectResolver.BindModel(Type type, IRequestData data)
at FubuMVC.Core.Diagnostics.RecordingObjectResolver.BindModel(Type type, IRequestData data)
at FubuMVC.Core.Runtime.FubuRequest.<>c__DisplayClass2.<.ctor>b__0(Type type)
at FubuMVC.Core.Util.Cache`2.Retrieve(KEY key)
at FubuMVC.Core.Util.Cache`2.get_Item(KEY key)
at FubuMVC.Core.Runtime.FubuRequest.Get[T]()
Jon:
PartialForEach does not new up new models, it passes the models you pass (i.e. x.Orders).
It looks like the problem you're facing is because you did not add ".Using" in your CustomerPartial.ascx
In CustomerPartial.ascx, change
<%= this.PartialForEach(x => x.Orders) %>
to
<%= this.PartialForEach(x => x.Orders).Using<OrderPartial>() %>
Related
I am using ASP.Net MVC with C#. I have a model which has a member for filter criteria. This member is a IList>. The key contains value to display and the value tells if this filter is selected or not. I want to bind this to bunch of checkboxes on my view. This is how I did it.
<% for(int i=0;i<Model.customers.filterCriteria.Count;i++) { %>
<%=Html.CheckBoxFor(Model.customers.filterCriteria[i].value)%>
<%=Model.customers.filterCriteria[i].key%>
<% } %>
This displays all checkboxes properly. But when I submit my form, in controller, I get null for filtercriteria no matter what I select on view.
From this post I got a hint for creating separate property. But how will this work for IList..? Any suggestions please?
The problem with the KeyValuePair<TKey, TValue> structure is that it has private setters meaning that the default model binder cannot set their values in the POST action. It has a special constructor that need to be used allowing to pass the key and the value but of course the default model binder has no knowledge of this constructor and it uses the default one, so unless you write a custom model binder for this type you won't be able to use it.
I would recommend you using a custom type instead of KeyValuePair<TKey, TValue>.
So as always we start with a view model:
public class Item
{
public string Name { get; set; }
public bool Value { get; set; }
}
public class MyViewModel
{
public IList<Item> FilterCriteria { get; set; }
}
then a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
FilterCriteria = new[]
{
new Item { Name = "Criteria 1", Value = true },
new Item { Name = "Criteria 2", Value = false },
new Item { Name = "Criteria 3", Value = true },
}
});
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// The model will be correctly bound here
return View(model);
}
}
and the corresponding ~/Views/Home/Index.aspx view:
<% using (Html.BeginForm()) { %>
<%= Html.EditorFor(x => x.FilterCriteria) %>
<input type="submit" value="OK" />
<% } %>
and finally we write a customized editor template for the Item type in ~/Views/Shared/EditorTemplates/Item.ascx or ~/Views/Home/EditorTemplates/Item.ascx (if this template is only specific to the Home controller and not reused):
<%# Control
Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<AppName.Models.Item>"
%>
<%= Html.CheckBoxFor(x => x.Value) %>
<%= Html.HiddenFor(x => x.Name) %>
<%= Html.Encode(Model.Name) %>
We have achieved two things: cleaned up the views from ugly for loops and made the model binder successfully bind the checkbox values in the POST action.
I have a ViewModel with a property as below:
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString="{0:d}", ApplyFormatInEditMode=true)]
public DateTime ConfigDate { get; set; }
The Form that displays the ConfigDate is as below:
<%= Html.EditorFor(Model => Model.ConfigDate)%>
When the Index Action comes back, everything looks formatted correctly, i.e. the <input> box has the date value as 12/12/2001. When the form is posted, the result that comes back is as though the DisplayFormat attribute isn't being applied.
EDIT:
More info was requested: here is the code en toto:
The Search Form
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Config.Web.Models.AirplanesViewModel>" %>
<% using (Html.BeginForm("Details", "Airplanes", FormMethod.Post, new { id = "SearchForm" })) { %>
<%= Html.LabelFor(model => model.ConfigDate) %>
<%= Html.EditorFor(Model => Model.ConfigDate)%>
<input id="searchButton" type="submit" value="Search" />
<% } %>
The AirplanesViewModel
public class AirplanesViewModel
{
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString="{0:d}", ApplyFormatInEditMode=true)]
public DateTime ConfigDate { get; set; }
}
}
The Controller
[HttpGet]
public ActionResult Index()
{
AirplanesViewModel avm = new AirplanesViewModel
{
ConfigDate = DateTime.Now
};
return View(avm);
}
[HttpPost]
[ActionName("Details")]
public ActionResult Details_Post(AirplanesViewModel avm)
{
return RedirectToAction("Details", avm);
}
[HttpGet]
public ActionResult Details(AirplanesViewModel avm)
{
int page = 0;
int pageSize = 10;
if (!ModelState.IsValid)
{
avm.Airplanes = new PaginatedList<Airplane>();
return View(avm);
}
try
{
Query q = new Query(avm.Query);
PaginatedList<Airplane> paginatedPlanes = new PaginatedList<Airplane>(repo.ByQuery(q), page, pageSize);
avm.Airplanes = paginatedPlanes;
return View(avm);
}
catch (Exception)
{
// Should log exception
avm.Airplanes = new PaginatedList<Airplane>();
return View(avm);
}
}
Additional Information
It has something to do with the redirection to the GET Action. When I take out the POST Action and remove the GET attribute (so both GET and POST use the Details() Action) the problem goes away - but this also gets rid of my pretty URL's when the form is submitted (and causes the annoying "are you sure?" popup on refresh). Strangely, the only problem is the loss of formatting in that field. Everything else is fine.
While waiting for you to clearly specify the problem, here's a full working counter example that what you describe in your question doesn't actually happen:
Model:
public class MyViewModel
{
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime ConfigDate { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
ConfigDate = DateTime.Now
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
View:
<% using (Html.BeginForm()) { %>
<%= Html.EditorFor(x => x.ConfigDate) %>
<input type="submit" value="OK" />
<% } %>
You can submit the form as much as you wish, the formatting will be preserved.
UPDATE:
After providing additional information here's why the problem occurs. When you redirect to the Details action with return RedirectToAction("Details", avm); a query string parameter is applied to the url:
http://localhost:1114/Airplanes/Details?ConfigDate=11/30/2010%2000:00:00
Notice how the hour is included and that's normal. Now when you return the view in the Details GET action the HTML helper responsible for generating the editor template will do the following tasks:
Check to see whether there's a ConfigDate parameter (either GET or POST). If none was found check the value of the Model which is passed to the view and use the ConfigValue property of the model and generate the textbox.
As a ConfigValue is found in the query string the model is not used at all. So it simply takes the value passed in the redirect which also contains the time and uses it to bind to it.
I`m trying to display classes that have properties of type some custom class inside it.
Model:
public class ComplexParent
{
public SimpleChild First { get; set; }
public SimpleChild Second { get; set; }
}
public class SimpleChild
{
public int Id { get; set; }
public string ChildName { get; set; }
public string ChildDescription { get; set; }
}
Controller:
public ActionResult Testify(int id)
{
ComplexParent par = new ComplexParent();
par.First = new SimpleChild() { Id = id };
par.Second = new SimpleChild()
{
Id = id + 1,
ChildName = "Bob",
ChildDescription = "Second"
};
return View("Testify", par);
}
[HttpPost]
public ActionResult Testify(ComplexParent pComplexParent)
{
return View("Testify", pComplexParent);
}
View:
<% using (Html.BeginForm())
{%>
<fieldset>
<legend>Fields</legend>
<%: Html.EditorFor(x => x.First) %>
<br />
<%: Html.EditorFor(x => x.Second.ChildName)%>
<br/>
<br/>
<br/>
<% Html.RenderPartial("SimpleChild", Model.First); %>
<p>
<input type="submit" value="Watch me :-)" />
</p>
</fieldset>
<% } %>
When it comes to Get it works just fine, I can see all the data. But on post pComplexParent parameter is empty (both properties of a complex class are nulls). Probably Im missing something here, but could not get this to work ...
Small addition: view part that only shows editor for name makes Second child not null and name is set to Bob. But I dont understand how to make it just with EditorFor or DisplayFor methods.
UPDATE: Thanks to Darin Dimitrov, who kindly went throught all my code and found what caused this problem. The exact problem was that if you are using display template, asp.net mvc 2 doesnt post any values back and if whole template is has nothing to post back object is null. I still thinking of the way how to get the data, even if you don`t want to edit it. But using editor template does the thing and I have all objects filled with proper data now.
Your view is a bit of a mess. You are using editor templates along with partials for the first child. It is not very clear what fields are included inside the form. I would recommend you using only editor templates:
Model:
public class ComplexParent
{
public SimpleChild First { get; set; }
public SimpleChild Second { get; set; }
}
public class SimpleChild
{
public int Id { get; set; }
public string ChildName { get; set; }
public string ChildDescription { get; set; }
}
Controller:
[HandleError]
public class HomeController : Controller
{
public ActionResult Testify(int id)
{
var par = new ComplexParent();
par.First = new SimpleChild() { Id = id };
par.Second = new SimpleChild()
{
Id = id + 1,
ChildName = "Bob",
ChildDescription = "Second"
};
return View(par);
}
[HttpPost]
public ActionResult Testify(ComplexParent pComplexParent)
{
return View(pComplexParent);
}
}
View:
<% using (Html.BeginForm()) { %>
<%: Html.EditorFor(x => x.First) %>
<%: Html.EditorFor(x => x.Second) %>
<input type="submit" value="Watch me :-)" />
<% } %>
Editor template for SimpleChild (~/Views/Home/EditorTemplates/SimpleChild.ascx):
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<SomeNs.Models.SimpleChild>" %>
<%: Html.HiddenFor(x => x.Id) %>
<%: Html.EditorFor(x => x.ChildName) %>
<%: Html.EditorFor(x => x.ChildDescription) %>
Now if you want to have different editor templates for the two child properties you could either specify the editor template name when including it:
<%: Html.EditorFor(x => x.First, "FirstChildEditor") %>
which corresponds to ~/Views/Home/EditorTemplates/FirstChildEditor.ascx or use an [UIHint] attribute at your model:
public class ComplexParent
{
[UIHint("FirstChildEditor")]
public SimpleChild First { get; set; }
public SimpleChild Second { get; set; }
}
My recommendation is not to use Html.RenderPartial for generating input fields because their names will be hardcoded and won't bind properly depending on your objects hierarchy.
I have a section of a view that I would like to submit to an action so it is setup inside a Html.BeginForm()
I am trying to also make use of the Html.Telerik().DatePicker() control and would like it also to strongly type linked inside the form, same as the DropDownListFor, is this possible?
<% using (Html.BeginForm("MyAction", "Responding")) { %>
<%= Html.DropDownListFor(m => m.SelectedItem, Model.MyItems) %>
<!--list works fine-->
<% Html.Telerik().DatePicker()
.Name("datePicker")
.Render(); %>
<!--how do I get this strongly-type-linked?-->
<input type="submit" value="submit" />
<% } %>
The model for the view has the appropriate properties:
public int SelectedItem { get; set; }
public string SelectedDate { get; set; }
Controller code:
public class RespondingController : Controller
{
[HttpGet]
public ActionResult MyAction(int SelectedItem, string SelectedDate)
{
//on the form submit SelectedItem value arrives
//need SelectedDate to do the same
}
}
Your parameter is incorrect. it needs to be named the same as the name value you gave to the control e.g 'datePicker'.
public class RespondingController : Controller
{
[HttpGet]
public ActionResult MyAction(int SelectedItem, DateTime? datePicker)
{
//on the form submit SelectedItem value arrives
//need SelectedDate to do the same
}
}
It seems like it's not possible to edit custom object anymore after I upgraded to asp.net mvc 2 rc 2? I use this approach http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-4-custom-object-templates.html with a custom object like this:
My model has just one property but inherits from an abstract base class
public class Page : ContentItem {
[DataType(DataType.MultilineText)]
public virtual string MainIntro { get;set; } // This property render correct
[DisplayFormat(NullDisplayText="(null value)")]
public virtual DetailCollection Tags { get; set; }
}
My controller looks like this
public ActionResult Edit(string pagePath) {
var page = _repository.GetByUrlSegment(pagePath);
return View(page.EditViewName, new DashboardModel(page, RootPages));
}
And my view looks like this
<% using (Html.BeginForm("update","Dashboard", FormMethod.Post, new { name = "editForm" } )) %>
<% { %>
<div>
<%=Html.EditorFor(model => model.CurrentItem) %>
<div class="editor-button">
<input type="submit" value="Save" />
</div>
</div>
<% } %>
Perhaps it would be better to expose this to the view as a space-separated string and exclude the collection from being displayed in the view. Alternatively, you might be able to define a specific template for how you want to display a collection. It's not clear to me how MVC would be able to determine what to display otherwise.
Try something like:
[ShowForDisplay(false)]
[ShowForEdit(false)]
public virtual DetailCollection Tags { get; set; }
public virtual string TagList
{
get
{
if (tags == null) return "(null value)";
// assumes DetailCollection implements IEnumerable<string>
return string.Join( " ", tags.Select( t => t).ToArray() );
}
set
{
tags = new DetailCollection( value.Split( new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries )
.Select( s => s.Trim() ) );
}
}