ASP.NET MVC 4 generating a treeview with recursive partial view - asp.net-mvc

I have a partial view in an MVC 4 project, which is strongly typed. It takes an IEnumerable collection of a table of a database. In that table there are IDs, Names, and ParentIDs for storing hierarchical connection between records. The view that calls the partial view is also strongly typed, it takes the whole database as the model, and passes the Categories table to the partial view, as an enumerable collection:
#Html.Partial("_TreeCategories", #Model.Categories.ToList())
And in the partial view, I want to take the root nodes first, so I can extend the whole tree in a recursive way. In the database table, all records are considered as root nodes with a ParentID == null.
So generally, my way to do this would look like:
#model IEnumerable<TreeCollections.OpenAccess.Category>
#if (Model.ToList().Count >= 0)
{
#if (Model.ToList()[0].Parent_id == null)
{
<text><ul id="navigation"></text>
}
#foreach (var node in #Model)
{
<li>#node.Name
#foreach (var subNode in #Model.Where(s => s.Parent_id == node.Id))
{
#Html.Partial("_TreeCategories", subNode)
}
</li>
}
#if (Model.ToList()[0].Parent_id == null)
{
</ul>
}
}
So I check if the first element's ParentID of the Model is null, and if it is, then it should create a < ul> tag with the id "navigation", so the jquery plugin can recognise that it is meant to be a treeview control. Then it creates a list tag with a recursive call within. The recursively called partial view takes the children of the node as the model. And lastly if we arrived to the end of the partial view's rendering, and we are at the "root level", it should write a closing < ul> tag
There are some problems, however. First, at the end, that closing unordered list tag is wrong, VS can't find the matching start tag for that. Second, I don't know why, but at the top, I can put the starter < ul> tag in between tags, and I can't do it at the closing tag below. But I'm not sure about these < ul > tags either, I feel those are wrong too.
Please, help me, I'm stuck with this for days now.

man, you got some wonk going on here. i feel your pain on getting stuck.
see if this floats your boat.
you need a seed value to keep track of what you are looking for in the listing when you do recursion on the same list. it's better to do a parent children mapping in the class, but meh this was fun to do given your structure and should do the trick.
Models
namespace trash.Models
{
public class Category
{
public int ID { get; set; }
public int? Parent_ID { get; set; }
public string Name {get; set;}
}
public class SeededCategories
{
public int? Seed { get; set; }
public IList<Category> Categories { get; set; }
}
}
Controller (NOTE: you start the recursion chain by setting the Seed property to null which will pick up all the null parents)
namespace trash.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
IList<trash.Models.Category> categories = new List<trash.Models.Category>();
categories.Add(new trash.Models.Category { ID = 1, Parent_ID = null, Name = "Top1" });
categories.Add(new trash.Models.Category { ID = 2, Parent_ID = null, Name = "Top2" });
categories.Add(new trash.Models.Category { ID = 3, Parent_ID = 1, Name = "Top1Ring1" });
categories.Add(new trash.Models.Category { ID = 4, Parent_ID = 1, Name = "Top1Ring2" });
trash.Models.SeededCategories model = new Models.SeededCategories { Seed = null, Categories = categories };
return View(model);
}
}
}
Views
Index
#model trash.Models.SeededCategories
Here's a list
#Html.Partial("_TreeCategories", Model)
Partial (your _TreeCategories. NOTE: set the Seed to the current node ID and volia recursion)
#model trash.Models.SeededCategories
#if (Model.Categories.Where(s => s.Parent_ID == Model.Seed).Any())
{
<ul>
#foreach (var node in Model.Categories)
{
if (node.Parent_ID == Model.Seed)
{
trash.Models.SeededCategories inner = new trash.Models.SeededCategories { Seed = node.ID, Categories = Model.Categories };
<li>#node.Name
#Html.Partial("_TreeCategories", inner)
</li>
}
}
</ul>
}

You can try Shield UI's recursive TreeView for ASP.NET MVC.
It allows you to specify all the TreeView items using a RecursiveDataSource object, which can be setup to retrieve the data for a tree item from a remote endpoint or a local source "lazily", whenever the item is being expanded.
The RecursiveDataSource is a wrapper around a JavaScript DS widget, which introduces the need for some JS code, as well as updating your server code that will provide the data (either implementing a web service, or place the data in a JS variable in your view).

Related

Error when passing multiple tables from controller to view MVC4

I tried to pass 2 tables from Controller to view by using ViewModel
I declared:
public class Temp
{
public Models.ORDER_HEADER_INFO ORDER_HEADER_INFO { get; set; }
public Models.ORDER_ROUTING_DETAIL ORDER_ROUTING_DETAIL { get; set; }
}
In controller I write:
public ActionResult DataLoading()
{
using (Models.AllDataEntities et = new Models.AllDataEntities())
{
var Odata = (from ord in et.ORDER_ROUTING_DETAIL join
oh in et.ORDER_HEADER_INFO on ord.ORDER_NO equals oh.ORDER_NO
orderby ord.TARGET_COMPLETION_FLAG,oh.PRODUCT_START_DATE
select new {Order_No = oh.ORDER_NO,ROUTING_NAME = ord.ROUTING_NAME,
PJNO = oh.PJNO,DELIVERY_DESTINATION = oh.DELIVERY_DESTINATION,
}).ToList();
return View(Odata);
}
}
In view:
<table>
#for (int i = 0; i < Model.Count; i++ )
{
<tr>
<td>#Html.DisplayFor(m=>m[i].ORDER_HEADER_INFO.ORDER_NO)</td>
<td>#Html.DisplayFor(m=>m[i].ORDER_HEADER_INFO.PJNO)</td>
<td>#Html.DisplayFor(m=>m[i].ORDER_ROUTING_DETAIL.ROUTING_NAME)</td>
<td>#Html.DisplayFor(m=>m[i].ORDER_HEADER_INFO.DELIVERY_DESTINATION)</td>
</tr>
}
</table>
When i run the code, there is the exception like this:
The model item passed into the dictionary is of type
'System.Collections.Generic.List1[<>f__AnonymousType34[System.String,System.String,System.String,System.String]]',
but this dictionary requires a model item of type
'System.Collections.Generic.List`1[TIS.Models.Temp]'.
When I debug, Data is completely loaded to Odata, but I cannot understand what type of Odata.
You need to have this select statement. Instead of Anonymous Type, Select new Temp() { ... }. Reason is that your View Expects List<temp>, but you are passing list<AnonymousType> from controller.
var Odata = (from ord in et.ORDER_ROUTING_DETAIL join
oh in et.ORDER_HEADER_INFO on ord.ORDER_NO equals oh.ORDER_NO
orderby ord.TARGET_COMPLETION_FLAG,oh.PRODUCT_START_DATE
select new Temp() {ORDER_HEADER_INFO = oh, ORDER_ROUTING_DETAIL = ord }).ToList();
If you dont not want to use all the properties in the entities, then you need to create your DTO with the properties you want to use and map them in the linq query.
Your server side code should instantiate the Temp object while creating the list:
public ActionResult DataLoading()
{
using (Models.AllDataEntities et = new Models.AllDataEntities())
{
var Odata = (from ord in et.ORDER_ROUTING_DETAIL join
oh in et.ORDER_HEADER_INFO on ord.ORDER_NO equals oh.ORDER_NO
orderby ord.TARGET_COMPLETION_FLAG, oh.PRODUCT_START_DATE
select new Temp()
{
ORDER_HEADER_INFO = new ORDER_HEADER_INFO()
{
ROUTING_NAME = ord.ROUTING_NAME
},
ORDER_ROUTING_DETAIL = new ORDER_ROUTING_DETAIL()
{
Order_No = oh.ORDER_NO
}
}).ToList();
return View(Odata);
}
}
Your view can operate on this list like so:
#model IEnumerable<MvcApplication16.Models.Temp>
<table>
#foreach (var item in Model)
{
<tr>
<td>#Html.DisplayFor(item.ORDER_HEADER_INFO.ORDER_NO)</td>
<td>#Html.DisplayFor(item.ORDER_HEADER_INFO.PJNO)</td>
<td>#Html.DisplayFor(item.ORDER_ROUTING_DETAIL.ROUTING_NAME)</td>
<td>#Html.DisplayFor(item.ORDER_HEADER_INFO.DELIVERY_DESTINATION)</td>
</tr>
}
</table>
Note: As in your original Temp object definition, you have mapped entity directly, LINQ will throw error as you mentioned in the comment.
The entity or complex type 'Model.ORDER_HEADER_INFO' cannot be
constructed in a LINQ to Entities query.
You cannot (and should not be able to) project onto a mapped entity. You can, however, project onto an anonymous type or onto a DTO.
I suggest you create you modify your Temp definition and include properties that your want to send to view. That will make it a ViewModel object which is the right approach.
public class Temp
{
public string ORDER_NO { get; set; }
public string ROUTING_NAME { get; set; }
}

Rebinding MVC Models with multiple array selections

I have read somewhat on the post-redirect-get design pattern and I'm not sure if it works for my purpose as what I have is an MVC site which is design to look like an application, I have multiple dropdowns on the page which all bind to an integer array as below in my controller:
[HttpPost]
public ViewResult ResponseForm(PartyInvites.Models.GuestResponse response, int[] SelectedCustomer)
{
return View(response); // works but resets all my selected dropdowns
// return View(); // gives an error that it can't rebind items in view
}
My View:
#foreach (Schedule sched in Model.Schedules)
{
#Html.DropDownList("MySelectedCustomer", new SelectList(sched.Customers, "Id", "FirstName"), "Select A Customer", new { #class = "SelectedCustomer" })
}
The GuestResponse:
public class GuestResponse
{
[Required(ErrorMessage = "You must enter your name")]
public string Name { get; set; }
public string SomeString = "someString";
public string Email { get; set; }
public string Phone { get; set; }
public bool? WillAttend { get; set; }
public int SelectedSchedule = 0;
public int SelectedCustomer = 0;
public List<Schedule> Schedules
{
get
{
return new List<Schedule>() { new Schedule() { ScheduleName = "party1", ScheduleId = 1 }, new Schedule() { ScheduleId = 2, ScheduleName = "party2" } };
}
set
{
Schedules = value;
}
}
}
The SelectCustomer property is a property on the GuestResponse class. All the dropdowns are bound and if I change a few they bind nicely to the int[] SelectedCustomer collection. However I want to return my View back (so it does nothing essentially) but this resets all the dropdowns to their original state as the response was never fully bound because there was multiple dropdowns and MVC couldn't model bind to it. What it the best way of doing this so it maintains state so to speak?
The correct way to handle this is to use a view model instead of passing your domain models to the view.
But if you don't want to follow good practices you could generate your dropdowns like this as a workaround:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownList(
"MySelectedCustomer[" + i + "]",
new SelectList(
Model.Schedules[i].Customers,
"Id",
"FirstName",
Request["MySelectedCustomer[" + i + "]"]
),
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
The correct way is to have a property of type int[] SelectedCustomers on your view model and use the strongly typed version of the DropDownListFor helper:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownListFor(
x => x.SelectedCustomers,
Model.Schedules[i].AvailableCustomers,
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
and your POST controller action will obviously take the view model you defined as parameter:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
// The model.SelectedCustomers collection will contain the ids of the selected
// customers in the dropdowns
return View(model);
}
And since you mentioned the Redirect-After-Post design pattern, this is indeed the correct pattern to be used. In case of success you should redirect to a GET action:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
if (!ModelState.IsValid)
{
// the model is invalid => redisplay the view so that the user can fix
// the errors
return View(model);
}
// at this stage the model is valid => you could update your database with the selected
// values and redirect to some other controller action which in turn will fetch the values
// from the database and correctly rebind the model
GuestResponse domainModel = Mapper.Map<GuestResponseViewModel, GuestResponse>(model);
repository.Update(domainModel);
return RedirectToAction("Index");
}
Note: I'm first addressing why it's not binding anything, but that's not addressing the array issue, which I will get to afterwards. Where most people go wrong with MVC is that they do not take advantage of the built-in features of MVC to deal with these situations. They insist on doing foreach's and manually rendering things, but do not take into account the collection status.
The reason why the values are reset is because you are using Html.DropDownList() rather than Html.DropDownListFor(), and you are renaming the posted property name to a different name than your model property name.
You could simply change it to this:
#Html.DropDownList("SelectedCustomer", // note the removal of "My"
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
However, you would not have had this issue, and saved yourself a huge headache if you had just used the strongly typed version.
#Html.DropDownListFor(x => x.SelectedCustomer,
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
As for the Array, you should use an EditorTemplate for Schedules, and in that EditorTemplate you simply create your html as if it were a single item. That's the great thing about Editor/DisplayTemplates is that they automatically deal with collections.
Create a folder in your Views/Controller folder called EditorTemplates. In that folder, create an empty file called Schedule.cshtml (assuming Schedules is a List or array of Schedule). In that, you have code to render a single schedule.
EDIT:
Darin brings up a good point. I would make a small change to the model and add a Selected property to both Schedule and GuestResponse, then you can use Linq to return the selected schedule and it would simplify things.
EDIT2:
You some conflicts between the problem you've described and the code you've shown. I suggest you figure out exactly what you're trying to do, since your code does not really reflect a viable model for this.

MVC pass viewmodel view with viewmodel containing parent values and child list

My application has multiple areas within one facility. I am trying to pass a single model to the view that contains facility values (facility_id, facility_name), and a list of areas. I currently have the list of areas as a type of the entity model for the table (area_list).
My viewmodel is as follows:
public class AreaView
{
public string facility_name { get; set; }
public int facility_id { get; set; }
public int group_id { get; set; }
public IList<area_list> areas { get; set; }
}
As an aside, I had originally tried setup the list of areas as a separate viewmodel (AreaS) instead of the model area_list, but I had other issues there so went back to directly referencing the for simplicity. I am assuming this would be more appropriate...
My Controller:
public ActionResult List(int id = 0)
{
var model = (from f in areaDB.facility_list
where f.facility_id == id
select new AreaView
{
facility_id = f.facility_id,
facility_name = f.facility_name,
areas = (from a in areaDB.area_list
orderby a.area_name
where a.facility_id == id
select a).ToList()
});
return View(model);
}
My View (abbreviated):
#model SkyeEnergy.Models.AreaView
Facility: #Model.facility_name
#foreach (var item in Model.areas) {
<tr>
<td>
#Html.ActionLink(item.vendor_name,"Details","Area",new {id = item.vendor_id},null)
</td>
</tr>
}
I have tried numerous variations to accomplish what's below, which has given me numerous errors, but the most recent is below:
The model item passed into the dictionary is of type
'System.Data.Entity.Infrastructure.DbQuery`1[MyApp.Models.AreaView]',
but this dictionary requires a model item of type
'MyApp.Models.AreaView'.
I understand that I am not passing the correct type for what the view is expecting, but I cannot seem to figure out:
Is the viewmodel setup correctly in the firstplace (how to mix values and a list of children
How to structure my linq query to get one AreaView object with
all my data
Pass it appropriately (in the correct type) to my
view
I have read about 45 posts on Stackoverflow, but can't seem to piece them together to accomplish what's above. If anyone has a correct solution (or even a direction), I would be very appreciative.
Thanks for any help.
I think you should add FirstOrDefault() at the end of your query to return the AreaView
public ActionResult List(int id = 0)
{
var model = (from f in areaDB.facility_list
where f.facility_id == id
select new AreaView
{
facility_id = f.facility_id,
facility_name = f.facility_name,
areas = (from a in areaDB.area_list
orderby a.area_name
where a.facility_id == id
select a).ToList()
}).FirstOrDefault();
return View(model);
}
I would not combine both object in the same query. I would do
1) Select AreaView where id = xxxx
2) Select Areas where id = xxxx
3) Assign areas to my AreaView
Example
AreaView model = GetAreaView(id);
model.Areas = GetAreas(id);
return View(model);
Also, try the following for your current code
return View(model.FirstOrDefault());

Can I pass a different model to my grid when using HTML.DisplayFor?

I have an application that shows a grid/table of questions and each question has a status dropdown. There are around 1-200 questions and each status drop down has about 50 choices that are the same for every row of the grid.
My controller passes the following model to a view:
IEnumerable<Question.Grid>
Then in my view I have the following code that prints out the detail lines of a grid table:
<tbody class="grid">
#if (Model != null) {
foreach (var item in Model) {
#Html.DisplayFor(model => item, "QuestionDetail")
}
}
</tbody>
Each of the grid lines has a status dropdown and I would like to pass the data for the dropdown (same for every row) to the QuestionDetail view. What's the best way for me to send this additional information so that in my view I can have something like the following:
#Html.DropDownList("Question.Status", Status, new { id = "StatusID"})
First of all, don't pass IENumerable of view models. Rather define one that has IEnumerable as property like this:
public class EnumViewModel
{
public IEnumerable<Question.Grid> Questions { get; set; }
public int MyAdditionalFieldIWantedToPassAlong { get; set; }
}
public class Question.Grid
{
public string MyExistingFields { get; set; }
// as many as you had
public string MyAdditionalFields { get; set; }
// as much as you want
}
and have your view receive one model instead of list of models as #model EnumViewModel.
At this point it must be very easy for you to add any additional information as your heart pleases inside the EnumViewModel should this information belong there. If its more specific to Questions,put it there and enjoy your items inside every model in the list containing that StatusID.
Think of it as just a container for you to hold data your view needs to
display and
post back to controller
Hope this helps.

Validation ASP.NET MVC Child Elements

I'm currently working on an ASP.NET MVC 3 application. I'm building a screen with out-of-the-box validation. Allow me to clarify the situation using the following screenshot.
Above you see a collection of TemplateItems. The second column 'Uitvoerder' is required. This works fine in most cases.
The problem however, is that it's not a regular list of items, but that it's structured to represent the hierarchy shown in the UI. Hence the second item is a child of the first, and thus contained in the first TemplateItem object you see.
Validation does not fire for the contained items.
You could argue that the front end model should be flattened and made less complex, but I'd like to avoid that. Is there any way I can have the validation trigger for the child elements as well?
The model looks like this:
public class WorkflowTemplateItemModel
: IValidatableObject
{
public WorkflowTemplateItemModel[] ChildWorkflowTemplateItems { get; set; }
public long? ExecutionParticipantId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ExecutionParticipantId == null)
{
yield return new ValidationResult("Contact needs to be specified",new[] {"ExecutionParticipantId"});
}
}
}
The relevant Razor part:
<td>
#Html.DropDownListFor(model => model.ExecutionParticipantId,
Model.AvailableUsers.Select(user => new SelectListItem
{
Text = user.UserName,
Value = user.Id.ToString(),
Selected = (Model.ExecutionParticipantId == user.Id)
}),
string.Empty
)
</td>
and the razor which builds the tree view:
#for (int i = 0; i < Model.ChildWorkflowTemplateItems.Length; i++)
{
#Html.EditorFor(model => model.ChildWorkflowTemplateItems[i], new { Depth = Depth + 1, ParentId = Model.WorkflowItemId, RootModel = GetViewData<CreateWorkflowModel> ("RootModel") })
}
You can do that but you will need to create a custom validation including the client-side validation code.
The process is similar to this one: http://haacked.com/archive/2009/11/18/aspnetmvc2-custom-validation.aspx

Resources