As I am trying to create a TreeView folder structure in MVC. I have class file something like the below.
Class File
public class TreeViewFolder
{
public string FolderPath { get; set; }
public string FolderName { get; set; }
public List<TreeViewFolder> MyTreeList { get; set; }
}
I need to render that above list in MVC View. I have no idea how to render single linked list data in MVC View. Any help will be appreciated.
Thanks
You could create an extension method that uses recursion to build <ul> and <li> elements to show the hierarchy of folders
public static class FolderTreeExtensions
{
public static MvcHtmlString FolderTree(this HtmlHelper helper, TreeViewFolder folder)
{
return MvcHtmlString.Create(TreeLeaf(folder));
}
// Recursive function
private static string TreeLeaf(TreeViewFolder folder)
{
StringBuilder html = new StringBuilder();
TagBuilder div = new TagBuilder("div");
div.InnerHtml = folder.FolderName;
html.Append(div.ToString());
if (folder.MyTreeList != null)
{
foreach (TreeViewFolder subFolder in folder.MyTreeList)
{
html.Append(TreeLeaf(subFolder));
}
}
TagBuilder item = new TagBuilder("li");
item.InnerHtml = html.ToString();
TagBuilder container = new TagBuilder("ul");
container.InnerHtml = item.ToString();
return container.ToString();
}
}
Then in your controller, initialize and populate and instance of TreeViewFolder, and in the view
#model TreeViewFolder
....
#Html.FolderTree(Model)
Then style the elements to suit your requirements
Note: either add a using statement in the view or add a reference to the <namespaces> section of web.config
Related
I am trying to divide listed items into pages by special tags that must be established by custom TagHelper
I have a class to hold data for page and items that will be processed
namespace SportWeb.Models.ViewModels
{
public class PagingInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages { get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); } }
}
}
I am wraping it inside an other modelviewdata
namespace SportWeb.Models.ViewModels
{
public class ProductListViewModel
{
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfos { get; set; }
}
}
Then insert it into Controller Class to retrieve data and establishing logic
public class ProductController : Controller
{
private IProductRepository _iProductRepository;
int PageSize = 4;
public ProductController(IProductRepository iProductRepository)
{
_iProductRepository = iProductRepository;
}
public IActionResult List(int itemPage = 1) => View(new ProductListViewModel
{ Products = _iProductRepository
.List.OrderBy(p => p.ProductID)
.Skip((itemPage - 1) * PageSize)
.Take(PageSize),
PagingInfos = new PagingInfo {
CurrentPage = itemPage,
ItemsPerPage = PageSize,
TotalItems= _iProductRepository.List.Count()} });
}
}
And creating my TagHelper class
namespace SportWeb.InfraSturcture
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper :TagHelper
{
private IUrlHelperFactory _iUrlHelperFactory;
public PageLinkTagHelper(IUrlHelperFactory iUrlHelperFactory)
{
_iUrlHelperFactory = iUrlHelperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public PagingInfo PageModel { get; set; }
public string PageAction { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
IUrlHelper urlHelper = _iUrlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i=1; i<PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
tag.Attributes["href"] = urlHelper.Action(PageAction, new { itempPage = i });
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
}
and here is View page codes
ViewData["Title"] = "List";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#model ProductListViewModel
#addTagHelper SportWeb.InfraStructure.*,SportStore
<h1>List</h1>
#foreach (var p in Model.Products)
{
<div>
<h3>#p.Name</h3>
#p.Description
<h4>#p.Price.ToString("c")</h4>
</div>
}
<div page-model="#Model.PagingInfos" page-action="List"></div>
ViewImport codes below
#using SportWeb.Models
#using SportWeb.Models.ViewModels
#using SportWeb.Entity
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#addTagHelper SportWeb.InfraStructure.*, SportWeb
But when program runs, navigation panel is not appearing on the page
Here navigation panel is not appearing
And When I open page source it seems Tag helper does not work , created tags are not added by the codes.
source page
I do not understand why my tag helper does not work at all. Do you have any idea about where I am making mistake ?
Edit : I am working with CORE 3.0 features. Can it be caused that problem ?
I tried to reproduce your scenario and it worked. I tried with NET Core 2.2 and Visual Studio 2017 15.9.11 and with .NET Core 3.0 Preview 5 with Visual Studio 2019 16.0.3.
Most likely the problem lies on your side. Try to troubleshoot. Start with checking if the tag helper is executed at all. Place a breakpoint in Process() method in your PageLinkTagHelper. See if it is being hit while running the application.
Double check if you are adding the tag helper properly. Properly added tag helper will have Visual Studio IntelliSense, like this:
#addTagHelper *, SportWeb this is answer of problem write it in ViewImport
I have read some similar topics here and on the web, but I don't think I have seen one that would classify this as a duplicate, so I am going to go ahead and post it. I am currently loading my dynamic menus from the database like so:
public void LoadMenus()
{
var dbContext = new ContentClassesDataContext();
var menus = from m in dbContext.Menus
where m.MenuName != "Home" && m.MenuGroup == "RazorHome" && m.RoleID == "Facility"
orderby m.Sequence, m.MenuName
select m;
var html = "";
if (menus.Any())
{
html += "<span/>";
foreach (var menu in menus)
{
html = html + $"<a href='{menu.URL}'>{menu.MenuName}</a><br/>";
}
html += "<hr>";
}
Session["Menus"] = html;
}
LoadMenus() is in my controller class, so I am not able (to my knowledge) to use Razor syntax. I would prefer to load the menus from the view instead, so that I am able to use #Html.ActionLink(linkText, actionName, controllerName). Loading the HTML the way I am currently doing it will generate different link text depending on the current controller, so the links are not always correctly rendered. Is it possible to access the database from the view? Or perhaps to just pass in the content from the database from the controller to the view and then render the menu that way?
You should keep your html in the cshtml views.
You should pass the data through the viewmodel and not through the session.
1)
In the controller, get the menu data (in this example we fetch some fake data).
Create a viewmodel that can hold the menu data and pass it to the view, as shown below:
public class HomeController : Controller
{
public ActionResult Index()
{
var menu = GetMenu();
var vm = new ViewModel() {Menu = menu};
return View(vm);
}
private Menu GetMenu()
{
var menu = new Menu();
var menuItems = new List<MenuItem>();
menuItems.Add(new MenuItem() { LinkText = "Home" , ActionName = "Index", ControllerName = "Home"});
menuItems.Add(new MenuItem() { LinkText = "About", ActionName = "About", ControllerName = "Home" });
menuItems.Add(new MenuItem() { LinkText = "Help", ActionName = "Help", ControllerName = "Home" });
menu.Items = menuItems;
return menu;
}
}
2)
This is the viewmodel
public class ViewModel
{
public Menu Menu { get; set; }
}
This view is an example of how you could render the menu data as a html menu
#model WebApplication1.Models.ViewModel
<ul id="menu">
#foreach (var item in #Model.Menu.Items)
{
<li>#Html.ActionLink(#item.LinkText, #item.ActionName,
#item.ControllerName)</li>
}
</ul>
3)
This is the example menu classes used (representing your entities from the dbcontext)
public class Menu
{
public List<MenuItem> Items { get; set; }
}
public class MenuItem
{
public string LinkText { get; set; }
public string ActionName { get; set; }
public string ControllerName { get; set; }
}
Here are some links to get you started:
http://www.codeproject.com/Articles/585873/Basic-Understanding-On-ASP-NET-MVC
http://www.asp.net/mvc/overview/getting-started/introduction/getting-started
Online describes
http://www.mikesdotnetting.com/article/275/custom-taghelpers-in-asp-net-mvc-6 how to create a pagination tag helper.
lines below
a.InnerHtml = i.ToString ();
li.InnerHtml = a.ToString ();
does not work.
Error : CS0200 C# Property or indexer 'TagBuilder.InnerHtml' cannot be assigned to -- it is read only
Also line
[TargetElement("pager", Attributes = "total-pages, current-page, url")]
has error
Old pagination options were easy setup and works exactly ... but the new version is the implementation leads
I have solved this:
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.AspNet.Mvc.Rendering;
using System.Text;
using Microsoft.Extensions.WebEncoders;
namespace MVC6_CustomTagHelper_Demo.TagHelpers
{
[HtmlTargetElement("pager", Attributes = "total-pages, current-page, link-url")]
public class PagerTagHelper : TagHelper
{
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
[HtmlAttributeName("link-url")]
public string Url { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.PreContent.SetHtmlContent("<ul class=\"link-list\">");
var items = new StringBuilder();
for (var i = 1; i <= TotalPages; i++)
{
var li = new TagBuilder("li");
var a = new TagBuilder("a");
a.MergeAttribute("href", $"{Url}?page={i}");
a.MergeAttribute("title", $"Click to go to page {i}");
a.InnerHtml.AppendHtml(i.ToString());
if (i == CurrentPage)
{
a.AddCssClass("active");
}
li.InnerHtml.Append(a);
var writer = new System.IO.StringWriter();
li.WriteTo(writer, new HtmlEncoder());
var s = writer.ToString();
items.AppendLine(s);
}
output.Content.SetHtmlContent(items.ToString());
output.PostContent.SetHtmlContent("</ul>");
output.Attributes.Clear();
output.Attributes.Add("class", "pager");
}
}
}
The InnerHtml no longer has a setter, but itself now has the methods Append, AppendLine, AppendFormat, AppendHtml and AppendHtmlLine. You can use these to inject strings directly into the TagBuilder.
The first can be particularly useful if you're extending the Html Razor directive (so #Html.MyMethod('foo') ), because you can hook into the any other Html methods, e.g. Partial.
Example - creating a utility extension for angular js templates which turns
#Html.AngularTemplate("FooBar")
into
<script type="text/ng-template" id="foo-bar.html">{{Content of FooBar.cshtml}}</script>
Thus -
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Extensions.WebEncoders;
using System;
using System.Text.RegularExpressions;
...
public static object AngularTemplate(this IHtmlHelper helper, string templateName)
{
var tagBuilder = new TagBuilder("script");
tagBuilder.MergeAttribute("type", "text/ng-template");
// turn CamelCase into camel-case
tagBuilder.MergeAttribute("id", Regex.Replace(templateName, #"([a-z])([A-Z])", "$1-$2").ToLowerInvariant() + ".html");
tagBuilder.InnerHtml.AppendHtml(helper.Partial(templateName));
tagBuilder.TagRenderMode = TagRenderMode.Normal;
tagBuilder.WriteTo(helper.ViewContext.Writer, HtmlEncoder.Default);
return null;
}
I'm kind of new to razor MVC, and I'm wondering how can I read the values I return in the view?
My code is like this:
public ActionResult Subject(int Category)
{
var db = new KnowledgeDBEntities();
var category = db.categories.Single(c => c.category_id == Category).name;
var items = from i in db.category_items
where i.category_id == Category
select new { ID = i.category_id, Name = i.name };
var entries = from e in db.item_entry
where items.Any(item => item.ID == e.category_item_id)
select new { ID = e.category_item_id, e.title };
db.Dispose();
var model = new { Name = category, Items = items, Entries = entries };
return View(model);
}
Basically, I return an anonymous type, what code do I have to write to read the values of the anonymous type in my view?
And if this is not possible, what would be the appropriate alternative?
Basically, I return an anonymous type
Nope. Ain't gonna work. Anonymous types are emitted as internal by the compiler and since ASP.NET compiles your views into separate assemblies at runtime they cannot access those anonymous types which live in the assembly that has defined them.
In a properly designed ASP.NET MVC application you work with view models. So you start by defining some:
public class MyViewModel
{
public string CategoryName { get; set; }
public IEnumerable<ItemViewModel> Items { get; set; }
public IEnumerable<EntryViewModel> Entries { get; set; }
}
public class ItemViewModel
{
public int ID { get; set; }
public string Name { get; set; }
}
public class EntryViewModel
{
public int ID { get; set; }
public string Title { get; set; }
}
and then you adapt your controller action to pass this view model to the view:
public ActionResult Subject(int Category)
{
using (var db = new KnowledgeDBEntities())
{
var category = db.categories.Single(c => c.category_id == Category).name;
var items =
from i in db.category_items
where i.category_id == Category
select new ItemViewModel
{
ID = i.category_id,
Name = i.name
};
var entries =
from e in db.item_entry
where items.Any(item => item.ID == e.category_item_id)
select new EntryViewModel
{
ID = e.category_item_id,
Title = e.title
};
var model = new MyViewModel
{
CategoryName = category,
Items = items.ToList(), // be eager
Entries = entries.ToList() // be eager
};
return View(model);
}
}
and finally you strongly type your view to the view model you have defined:
#model MyViewModel
#Model.Name
<h2>Items:</h2>
#foreach (var item in Model.Items)
{
<div>#item.Name</div>
}
<h2>Entries:</h2>
#foreach (var entry in Model.Entries)
{
<div>#entry.Title</div>
}
By the way to ease the mapping between your domain models and view models I would recommend you checking out AutoMapper.
Oh, and since writing foreach loops in a view is kinda ugly and not reusable I would recommend you using display/editor templates which would basically make you view look like this:
#model MyViewModel
#Model.Name
<h2>Items:</h2>
#Html.DisplayFor(x => x.Items)
<h2>Entries:</h2>
#Html.DisplayFor(x => x.Entries)
and then you would define the respective display templates which will be automatically rendered for each element of the respective collections:
~/Views/Shared/DisplayTemplates/ItemViewModel:
#model ItemViewModel
<div>#item.Name</div>
and ~/Views/Shared/DisplayTemplates/EntryViewModel:
#model EntryViewModel
<div>#item.Title</div>
Is it a code smell to have to following pattern, given the following code (highly simplified to get straight to the point) ?
The models :
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Category Cat { get; set; }
}
class Category
{
public int Id { get; set; }
public string Label { get; set; }
}
The view to edit a Product :
<% =Html.EditorFor( x => x.Name ) %>
<% =Html.EditorFor( x => x.Category ) %>
The EditorTemplate for Category
<% =Html.DropDownList<Category>() %>
The HtmlHelper method
public static MvcHtmlString DropDownList<TEntity>(this HtmlHelper helper)
where TEntity : Entity
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
return SelectExtensions.DropDownList(helper, "List", selectList, null, null);
}
For information, the real implementation of the helper method takes some lambdas to get the DataTextField and DataValueField names, the selected value, etc.
The point that bothers me is using a servicelocator inside the HtmlHelper. I think I should have a AllCategories property in my Product model, but I would need to be populated in the controller every time I need it.
So I think the solution I'm using is more straightforward, as the helper method is generic (and so is the modelbinder, not included here). So I just have to create an EditorTemplate for each type that needs a DropDownList.
Any advice ?
IMHO I'd leave it the way it is, have the same thing in another project.
BUT the service location bothered me as well so for another project I made this part of an ActionFilter which scans a model, finds all the anticipated dropdowns and does a batch load into ViewData. Since the ServiceLocator or Repository/Context/whatever is already injected into the Controller you don't have to spread your service location all over the place.
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
foreach( var anticipated in SomeDetectionMethod() )
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
ViewData["SelectList." + anticipated.Label/Name/Description"] = selectList;
}
}
In the view you can then make a helper to load up those dropdowns via a custom editor template or other method.
advice: look at the asp.net mvc sample application from here: http://valueinjecter.codeplex.com/
good luck ;)
This is how ValueInjecter's Sample Application could get the dropdowns:
(but it doesn't right now cuz I'm ok with the Resolve thing)
public class CountryToLookup : LoopValueInjection<Country, object>
{
ICountryRepo _repo;
public CountryToLookup(ICountryRepository repo)
{
_repo = repo;
}
protected override object SetValue(Country sourcePropertyValue)
{
var value = sourcePropertyValue ?? new Country();
var countries = _repo.GetAll().ToArray();
return
countries.Select(
o => new SelectListItem
{
Text = o.Name,
Value = o.Id.ToString(),
Selected = value.Id == o.Id
});
}
}