MVC 3 Regularly re-occurring elements...best practices - asp.net-mvc

On my site i wan't to display a list of categories from a db on almost every page. At the time being i use the ViewBag to store the categories, but I know there must be some better way. Therefore i'm wondering about best practices when loading re-occurring elements in MVC3.
Here's some code:
public class HomeController : Controller
{
private AlltForMusikContext db = new AlltForMusikContext();
//
// GET: /Admin/
public ViewResult Index()
{
var ads = db.Ads.Include(a => a.Category).OrderByDescending(a => a.Date);
ViewBag.Categories = db.Categories.ToList();
return View(ads.ToList());
}
public ViewResult Category(int id)
{
var ads = db.Ads.Where(a => a.Category.CategoryId == id).OrderByDescending(a => a.Date);
ViewBag.Categories = db.Categories.ToList();
ViewBag.Category = db.Categories.Where(a => a.CategoryId == id).FirstOrDefault();
return View(ads.ToList());
}
}
I use this code in the _Layout.cshtml
#Html.Partial("_GetCategories", (IEnumerable<AlltForMusik.Models.Category>)#ViewBag.Categories)
This is my partial view that I load into the layout view:
#model IEnumerable<AlltForMusik.Models.Category>
#foreach (var cat in Model)
{
<img src="#Url.Content("~/Content/img/icon_arrow.gif")" />
#Html.ActionLink(cat.CategoryName, "Category", "Home", new { id = cat.CategoryId })<br />
}
This works but I have to load the categories into the ViewBag every time I wanna load a view, othervise I get an error.
What is the best way to load content like this?
Answer:
I followed the advice and used a HtmlHelper. I stumbled upon some problems at first because i hade referenced the HtmlHelper in System.Web.Webpages instead of System.Web.Mvc. Here's the code i'm using:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using AlltForMusik.Models;
using System.Web.Mvc;
namespace AlltForMusik.Helpers
{
public static class HtmlHelpers
{
public static string GetCategories(this HtmlHelper helper)
{
AlltForMusikContext db = new AlltForMusikContext();
var categories = db.Categories.OrderBy(a => a.CategoryName).ToList();
string htmlOutput = "";
foreach (var item in categories)
{
htmlOutput += item.CategoryName + "<br />";
}
return htmlOutput.ToString();
}
}
}

Create custom HttpHelper with Caching. for example ShowCategories(). and then just put it on the view or on the common layout like:
#Html.ShowCategories()

Related

ActionLink generate empty href using MVC 5 with Umbraco 7

I am using MVC 5 with Umbraco 7. Trying to use ActionLink in my View but the markup it generates have empty href. any idea how to get it working?
Start Date
View:
#Html.ActionLink(
"Start Date",
"SearchV1",
"SearchV1",
new { sitetypeid = #Request.QueryString["sitetypeid"], leaNo = #Request.QueryString["leaNo"], orderBy = "VacStart" },
null)
Controller:
public class SearchV1Controller : RenderMvcController
{
public override ActionResult Index(RenderModel model)
{
return base.Index(model);
}
public ActionResult SearchV1(RenderModel model, int sitetypeId , int leaNo, string orderBy = "VacRelDate")
{
List<GetJobSearchResults_Result> searchResultsList = Workflow.Vacancy.GetJobSearchResults(sitetypeId , leaNo, "SCH", orderBy, "desc");
ViewBag.leaNo = leaNo;
return View(searchResultsList);
}
}
This cannot be possible in Umbraco unless you inherit your controller class from Surface controller.

#model dynamic binding with arrays

I have a data class that I want to show via a View, allow the User to change it, and bring it back in via data binding. I want to do this via #model dynamic. Is this possible?
This is my model
public class MyData
{
public int A { get; set; }
public string B { get; set; }
}
Getting sent to a view like so
public ActionResult Index()
{
MyData[] myDataList = new MyData[2];
myDataList[0] = new MyData() { A = 2, B = "Fred" };
myDataList[1] = new MyData() { A = 3, B = "Sue" };
dynamic charts = (from ch in myDataList
select new
{
ch.A,
ch.B
}).AsEnumerable().Select(c => c.ToExpando());
return View(charts);
}
With the view like so (this is wrong)
#model IEnumerable<dynamic>
#foreach (dynamic item in Model)
{
<text>
#Html.TextBox("item.B")
</text>
}
I'm floundering a bit here as I don't know if this is even possible and I can't find many complex examples of #model dynamic
To be clear, this example is an academic one, what I want here is to understand what's possible with dynamic models in asp mvc
Any advice?
Thanks
Try this in your view:
#for(int i = 0; i < Model.Lenght; i++)
{
#Html.TextBoxFor(model => model[i].A)
#Html.TextBoxFor(model => model[i].B)
}
While you can get it to work, I wouldn't recommend it.
The simple fix IMHO is to use the ExpandoObject as model directly. There's no point in converting it to dynamic at all.

MVC 5 Multiple Models

Is it possible to have multiple #Model functions within one page?
I have a Views/Shared/_layout.cshtml page which has the following code to pull the navigation from an SQL Server 2012 database:
#model IEnumerable<WebApplication1.navigation_V>
<ul class="nav sf-menu clearfix">
#foreach (var item in Model) {
<li>#Html.MenuItem(item.title_TV, item.url_TV, "Home")</li>
}
</ul>
This works fine on all views within the Views/Home folder, however the View/Accounts/Login.cshtml file has the following code:
#model WebApplication1.Models.LoginViewModel
Now I get the following error when trying to view the Account/Login page:
The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[WebApplication1.navigation_V]', but this dictionary requires a model item of type 'WebApplication1.Models.LoginViewModel'.
When writing the code I am not getting any red underline squiggles, the error only fires when trying to access the Account/Login page. This navigation function must be viewable on all pages, what I'm dreading next is actually getting the rest of the page content from the database in to these pages.
Any help would be much appreciated :-)
For further information I have included more code.
WebApplication1Entities.cs
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
namespace WebApplication1
{
public partial class WebApplication1Entities : DbContext
{
public WebApplication1Entities()
: base("name=WebApplication1Entities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<navigation_V> navigation_V { get; set; }
}
}
navigation_V.cs
using System;
using System.Collections.Generic;
namespace WebApplication1
{
public partial class navigation_V
{
public int navigation_ID { get; set; }
public string title_TV { get; set; }
public string url_TV { get; set; }
}
}
Controllers/HomeController.cs:
public static class MenuExtensions
{
public static MvcHtmlString MenuItem(
this HtmlHelper htmlHelper,
string text,
string action,
string controller
)
{
var li = new TagBuilder("li");
var routeData = htmlHelper.ViewContext.RouteData;
var currentAction = routeData.GetRequiredString("action");
var currentController = routeData.GetRequiredString("controller");
if (string.Equals(currentAction, action, StringComparison.OrdinalIgnoreCase) &&
string.Equals(currentController, controller, StringComparison.OrdinalIgnoreCase))
{
li.AddCssClass("active");
}
li.InnerHtml = htmlHelper.ActionLink(text, action, controller).ToHtmlString();
return MvcHtmlString.Create(li.ToString());
}
}
namespace WebApplication1.Controllers
{
public class HomeController : Controller
{
private WebApplication1Entities db = new WebApplication1Entities();
public ActionResult Index()
{
return View(db.navigation_V.ToList());
}
}
}
Views/Shared/Layout.cshtml
#model IEnumerable<WebApplication1.navigation_V>
<ul class="nav sf-menu clearfix">
#foreach (var item in Model) {
<li>#Html.MenuItem(item.title_TV, item.url_TV, "Home")</li>
}
</ul>
I hope this make things a little clearer.
Instead of rendering navigation IEnumerable in layout you can write child action in your Home controller like this:
[ChildActionOnly]
public ActionResult Navigation()
{
var navigationModel = ..
// your code to retrieve IEnumerable<WebApplication1.navigation_V>
// from DB
return View(navigationModel);
}
View for this action is Views/Home/Navigation.cshtml:
#{ Layout = null; } // preventing recursive rendering
#model IEnumerable<WebApplication1.navigation_V>
<ul class="nav sf-menu clearfix">
#foreach (var item in Model) {
<li>#Html.MenuItem(item.title_TV, item.url_TV, "Home")</li>
}
</ul>
And code for _layout.cshtml, replace your old navigation code with this one:
#{ Html.RenderAction("Navigation", "Home"); }

ASP MVC3 - How to load a custom user defined layout for the page from a database?

I have online form builder appplication in ASP.NET MVC3 with Razor views.
It is similar to this - https://examples.wufoo.com/forms/workshop-registration/
I need users to be able to customize the page design.
Not only to upload a custom css, but also to customize the HTML page template.
Let's say users should have complete control on Layout's HTML for their custom webform page. User should be able to edit any HTML on the page, beside the form that is included into the layout.
I'm not sure how to do that with Razor and ASP.NET MVC 3.
Is it possible to:
load layout somewhere from database as string or whatever
replace some custom tags like "FORM1_INCLUDE" to
#Html.Partial("some_non_customizable_layout_for_form1")
use the result as a valid Layout file for the user's form page
Maybe 1-3 is not the best way to do what I need.
What can you suggest for such user defined page layout approach in ASP.NET MVC 3 with Razor views?
UPDATE 1
Using VirtualPathProvider I was able to load View from database, but it just returns text like:
#inherits System.Web.Mvc.WebViewPage
<body>
#Html.EditorFor(z => z.Customer)
</body>
and doesn't process any Razor syntax at all.
What could be the problem with it?
SOLVED:
Needed to place this line as the first one in Application_Start() method:
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
UPDATE 2
Custom View Provider is registered in Global.asax.cs as:
protected void Application_Start()
{
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
MyVirtualPathProvider code is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return true;
//return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return new MyVirtualFile(virtualPath, "");
//return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
And now for the Razor view code defined as a string above I get this exception:
"Compiler Error Message: CS1061: 'System.Web.Mvc.AjaxHelper' does not contain a definition for 'BeginForm' and no extension method 'BeginForm' accepting a first argument of type 'System.Web.Mvc.AjaxHelper' could be found (are you missing a using directive or an assembly reference?)"
And when I changed Razor View code to:
string _body = /*body +*/ #"
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<dynamic>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""ppg_op2_personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
I got a different error:
Type 'ASP._Page__appstart_cshtml' does not inherit from 'System.Web.WebPages.ApplicationStartPage'
When I change
#inherits System.Web.Mvc.WebViewPage
to
#inherits System.Web.WebPages.ApplicationStartPage
to fix the error above, I get a new one:
"Compiler Error Message: CS0103: The name 'Ajax' does not exist in the current context"
UPDATE3:
I tried to use base.XXX:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return true;
return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return new MyVirtualFile(virtualPath, "");
return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
In this case I get a view that is not parsed at all, this is what I get in the web browser:
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm("Submit", new AjaxOptions { UpdateTargetId = "main" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id="ppg_op2_personal_info" class="op2-block">
</div>
<!-- <PERSONAL_INFO> -->
You should create a virtual path provider which fetches your custom views from a database.
There are several questions here about them. Just search for VirtualPathProvider
Updates (from my comments to the question discussion)
The VirtualPathProvider must be registered in Application_Start using HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
The base class MUST be called for all files that you can't currently serve. This is required since there can only be one VirtualPathProvider. (You'll see lots of strange errors otherwise)
#model directive doesn't work in files that you serve. You must use #inherits System.Web.Mvc.WebViewPage<YourNameSpace.YourModelName> instead.
IIRC you also need to override GetCacheDependency and return null for your own resources.
depending on the degree of flexibility and customization you wish to give this can be a very simple to a very time consuming task.
If you want users to simply customize the HTML on the page then you can use a WYSIWYG editor and store the raw html in the database.
In the view, use
#Html.Raw(Model.body) // Where body is the field containing the wysiwyg content
This will render the markup as-is.
To include custom tag / replacement you will have to define a list of string constants that can be inserted while using the WYSIWYG editor. These can then be search - replaced when displaying.
For example in your controller or model:
model.body.replace("$[form1]", "<form action='something' method='post' name='form1'></form>");
Of course depending on the nature of your application you might want to re-factor this into some sort of tag => markup conversion which will allow you to add more custom tags and their respective real HTML markups.
Hope this helps, Cheers!
I think you have to use something like this:
http://vibrantcode.com/blog/2010/11/16/hosting-razor-outside-of-aspnet-revised-for-mvc3-rc.html
I've done something similar & you may fine that XML is very useful. You save the layout definition as XML to the DB. That means it's easy to manipulate, either directly or by serialise/deserialise into an object model.
When you want to display the page, use XSLT to transform your XML into HTML, applying styles etc to the output.

Change ordering in MVC3 Index view

Would like to have clickable column titles eg click on TagCode once and it orders by that, and again it reverses. Same for Number.
Using MVC3/Razor and LightSpeed (ORM).
I know that a grid eg http://mvccontrib.codeplex.com/ may be the way forward. But as I don't need paging or filtering, I'd like to keep it simple for now.
Problem Is there a simple example of code (maybe with an up/down icon) that would help?
#Dave
Sorry, I missed your main point in my first answer
If you want to implement sorting using pure MVC3,
I can do that with following steps
list action method passing sortcolumn and order info to viewpage via ViewBag
Html Helper method to build actionlink with column ordering display
list viewpage calling the helper method
I uploaded source code here
List action method
public ActionResult Index(string sortColumn, bool? asc)
{
if (string.IsNullOrWhiteSpace(sortColumn))
sortColumn = "Number";
asc = asc ?? true;
SortDirection sortDirection = asc == true ? SortDirection.Ascending : SortDirection.Descending;
var query = _service.GetTags().OrderBy(sortColumn, sortDirection);
return View(query);
}
Html helper method
public static MvcHtmlString ActionLinkWithColumnOrder(this HtmlHelper helper,
string columnName,string action,string currentColumn,bool currentOrder)
{
object routeValues;
object htmlAttributes = null;
if (columnName == currentColumn)
{
routeValues = new { sortColumn = columnName, asc = !currentOrder };
htmlAttributes = new { #class = currentOrder ? "sort_asc" : "sort_desc" };
}
else
{
routeValues = new { sortColumn = columnName };
}
return helper.ActionLink(columnName, action, routeValues, htmlAttributes);
}
List View page
...
#Html.ActionLinkWithColumnOrder("TagCode", "Index", (string)ViewBag.sortColumn, (bool)ViewBag.asc)
...
#Html.ActionLinkWithColumnOrder("Number", "Index", (string)ViewBag.sortColumn, (bool)ViewBag.asc)
Happy Mvcing!
Sangsu PARK (http://supremeware.blogspot.com)
#Dave
How about using mvccontribgrid with ordering only as follows:
IMO, Using mvccontribgrid may bring more simple code.
This is controller code for example
public class HomeController : Controller
{
private AlbumService _service;
public HomeController()
{
_service = new AlbumService();
}
public ActionResult Index(GridSortOptions gridSortOptions)
{
var vm = new ViewModel<Album>()
{
DefaultSort = "AlbumId",
GridSortOptions = gridSortOptions,
List = _service.GetAlbums()
.OrderBy(gridSortOptions.Column, gridSortOptions.Direction),
};
return View(vm);
}
public ActionResult Details(int id)
{
var album = _service.GetAlbum(id);
ViewBag.RouteDicForList = Request.QueryString.ToRouteDic();
return View(album);
}
}
I attached simple sort function source code here with simple service & EF4.
Also, I posted full featured mvccontrib grid filtering & paging article here.

Resources