ASP.NET MVC HtmlHelper extensions for YUI controls (Yahoo User Interfaces)? - asp.net-mvc

Has anyone written any HTMLHelper classes for MVC that help with Yahoo's User Interface Library?
For instance I have written a helper method to convert a 'menu model' into the HTML markup needed to support the Yahoo Menu Control. The MVC pattern works well here because obviously if I chose to switch to a different menu implementation I can just write a new helper and not touch the model.
This code works for me but isn't fully tested and you're welcome to use it.
First we need a simple data structure for the menu model itself. You would add this to your page model with the normal MVC conventions. For instance I access a list of menu items from my view via ViewData.Model.MainMenu.MenuOptions.
public class MenuItem
{
public string Text { get; set; }
public string Description { get; set; }
public string RouteURL { get; set; }
public bool SeparatorBefore { get; set; }
public List<MenuItem> MenuItems { get; set; }
}
Extension method. Put in a namespace that is accessible to your view.
public static class YUIExtensions
{
public static string RenderMenu(this HtmlHelper html, string id, List<MenuItem> menuItems)
{
// <div id="mnuTopNav" class="yuimenubar yuimenubarnav">
// <div class="bd">
// <ul class="first-of-type">
// <li class="yuimenubaritem first-of-type"><a class="yuimenubaritemlabel" href="#store">Store</a></li>
// <li class="yuimenubaritem"><a class="yuimenubaritemlabel" href="#products">Products</a>
// <div id="communication" class="yuimenu">
// <div class="bd">
// <ul>
// <li class="yuimenuitem"><a class="yuimenuitemlabel" href="http://360.yahoo.com">360°</a></li>
// <li class="yuimenuitem"><a class="yuimenuitemlabel" href="http://mobile.yahoo.com">Mobile</a></li>
// <li class="yuimenuitem"><a class="yuimenuitemlabel" href="http://www.flickr.com">Flickr Photo Sharing</a></li>
// </ul>
// </div>
// </div>
// </li>
// </ul>
// </div>
//</div>
int menuId = 0;
HtmlGenericControl menuControl = CreateControl(html, id, 0, ref menuId, menuItems);
// render to string
StringWriter sw = new StringWriter();
HtmlTextWriter tw = new HtmlTextWriter(sw);
tw.Indent = 1;
menuControl.RenderControl(tw);
return sw.ToString();
}
private static HtmlGenericControl CreateControl(HtmlHelper html, string id, int level, ref int menuId, List<MenuItem> currentItems)
{
var menu = new HtmlGenericControl("div");
menu.Attributes["class"] = (level == 0) ? "yuimenubar yuimenubarnav" : "yuimenu";
menu.Attributes["id"] = id;
var div_bd = new HtmlGenericControl("div");
menu.Controls.Add(div_bd);
div_bd.Attributes["class"] = "bd";
HtmlGenericControl ul = null;
int i = 0;
foreach (var menuItem in currentItems)
{
if (ul == null || menuItem.SeparatorBefore)
{
ul = new HtmlGenericControl("ul");
div_bd.Controls.Add(ul);
if (i == 0)
{
ul.Attributes["class"] = "first-of-type";
}
}
var menuItem_li = new HtmlGenericControl("li");
menuItem_li.Attributes["class"] = (level == 0) ? "yuimenubaritem" : "yuimenuitem";
if (i == 0)
{
menuItem_li.Attributes["class"] += " first-of-type";
}
ul.Controls.Add(menuItem_li);
var href = new HtmlGenericControl("a");
href.Attributes["class"] = (level == 0) ? "yuimenubaritemlabel" : "yuimenuitemlabel";
href.Attributes["href"] = menuItem.RouteURL;
href.InnerHtml = menuItem.Text;
menuItem_li.Controls.Add(href);
if (menuItem.MenuItems != null && menuItem.MenuItems.Count > 0)
{
menuItem_li.Controls.Add(CreateControl(html, id + "_" + (menuId++), level + 1, ref menuId, menuItem.MenuItems));
}
i++;
}
return menu;
}
}
Stick this code where you want to generate the menu in your view (I have this in a master page):
<%= Html.RenderMenu("mnuTopNav", ViewData.Model.MainMenu.MenuOptions) %>
If you're lazy, or don't know about YUI you'll need this too in your <HEAD>
<!-- Combo-handled YUI CSS files: -->
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/combo?2.6.0/build/menu/assets/skins/sam/menu.css">
<!-- Combo-handled YUI JS files: -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.6.0/build/yahoo-dom-event/yahoo-dom-event.js&2.6.0/build/container/container_core-min.js&2.6.0/build/menu/menu-min.js"></script>
This currently generates markup for top nav style navigation bar - but it could be easily modified.
I was hoping somebody else was doing the same for some of the other controls.
Seems like a good candidate for an open source project - but I dont have time to start that.
Implementation advice welcomed!

Last night I did some thinking about this and am wondering if there's even more opportunity here to make general purpose HTMLHelpers using YUI or whatever other Javascript/HTML widgets you want.
For instance, if there was an interface for IMenu and one for ITextBox, ICheckBox, IRichTextEditor, ICarousel, etc. much like your class for a MenuItem, then you could have a YUI implementation of each of those interfaces, one for JQuery, one for MooTools or one for just straight HTML/CSS.
Part of what sparked this is the generalization that articles like this: http://designingwebinterfaces.com/essential_controls are taking to UI controls on the web for rich web apps.
Those interfaces would contain all of the basic stuff that is obvious at first glance: Id, Name, Value, List, Style, OnChange, OnClick, etc. as well as less obvious stuff like ValidationRegex, HelpText, etc.
That would let you have a layer that converts a model object or model property into an ITextBox and not worry about which one of the implementations of the interface will actually be handling it. You could also easily switch to a new implementation if one came along that was better/faster/cooler.
You'd have to deal with what should happen if you give something like ValidationRegex to a barebones HTML implementation and it has no way to deal with it, but I think it's a path worth thinking about. I also think it might make more sense to implement this as separate from the existing HTMLHelper namespace by inheriting it or just reimplementing it, but I am often wrong at this kind of early idea stage of coming up with a solution.
The YUIAsp.NET stuff is interesting, but is more oriented to WebForms and user controls than the direction that ASP.NET MVC and even moreso with Fubu MVC recently are going.
I tinkered with this idea a little bit and am really intrigued with the possibilities.

Simon,
I'm not sure this is helpful with respect to the MVC question, but there is a good open-source project that aims to simplify working with YUI within .NET:
http://www.yuiasp.net/
Menu is one of the controls that they include.
At the very least, this may be a project you can contribute back to if your work adds a new dimension to what's already there.
-Eric

Related

Orchard - Querying the Part On A Layout File

I’ve gotten stuck on an “Object reference not set to an instance of an object” error message.
We’re trying to use a PagePart field that is attached to the Page type to dynamically link a CSS file in the HEAD of a layout file. See below code.
<!-- DYNAMIC CSS-->
var contentItem = Model.ContentItem;
var pagePart = (PagePart)contentItem.PagePart;
if (!String.IsNullOrWhiteSpace(pagePart.FestivalProgramName))
{
<link ref="#Url.Content("/Themes/MyTheme/Styles/festival-programs/" + pagePart.FestivalProgramName + ".css")" rel="stylesheet" type="text/css">
}
This is in a file called:
Layout.cshtml
Something is wrong about this (obviously) since pagePart is “null” when I Attach to Debugger and look. I get that the Layout file doesn’t know that it’s associated with a “Page” Content Type but this layout is only used with Pages. Anyway, this is very similar to code that works elsewhere in our Orchard site. Any help or advice is hugely appreciated!
Thanks, T
In the Layout, the Model is the Layout object. It has nothing to do whatsoever with whatever content is going to get rendered into the Content zone.
I think what you are trying to do should be done by overriding the Page template (Content-Page.Detail.cshtml). (Note the Detail part, you probably don't want to import every css when displaying multiple pages in summary)
In there you can do:
#{
var contentItem = Model.ContentItem; // The Page content item
var pagePart = contentItem.Page; // Note that casting to PagePart won't work, because it does not exist
if (!String.IsNullOrWhiteSpace(pagePart.FestivalProgramName.Value))
{
// "Orchard's" way to include styles
Style.Include("festival-programs/" + pagePart.FestivalProgramName.Value + ".css");
}
}
EDIT:
What you probably should do (I assume not every page is a festival page) is create a new Content Type: FestivalPage. Then attach the following parts to this content type (same as the Page content type):
Common
Publish later
Title
Autoroute
Body (Orchard 1.8.1 and lower)
Layout (Orchard 1.9.x)
Tags
Localization
Menu
And your field:
FestivalProgramName
Then create an alternate Content-FestivalPage.Detail.cshtml with the following content:
#using Orchard.Utility.Extensions;
#{
if (Model.Title != null) {
Layout.Title = Model.Title;
}
Model.Classes.Add("content-item");
var contentTypeClassName = ((string)Model.ContentItem.ContentType).HtmlClassify();
Model.Classes.Add(contentTypeClassName);
var tag = Tag(Model, "article");
var contentItem = Model.ContentItem; // The FestivalPage content item
var pagePart = contentItem.FestivalPage; // The FestivalPage part with the FestivalProgramName field
if (!String.IsNullOrWhiteSpace(pagePart.FestivalProgramName.Value))
{
// "Orchard's" way to include styles
Style.Include("festival-programs/" + pagePart.FestivalProgramName.Value + ".css");
}
}
// -- Default Orchard content --
#tag.StartElement
<header>
#Display(Model.Header)
#if (Model.Meta != null) {
<div class="metadata">
#Display(Model.Meta)
</div>
}
</header>
#Display(Model.Content)
#if(Model.Footer != null) {
<footer>
#Display(Model.Footer)
</footer>
}
#tag.EndElement
// ----
This way you won't get in the way with the normal pages of the application.
Thank you so much for responding. Your solution works but not in my case since I use the Layout Selector module and can't override at the Page.Detail level since that would imply one layout - or at least from my perspective it seems that way. I found another option though that does the trick.
We already take advantage of the Handlers class to insert META stuff into the HEAD of the page and thanks to your feedback and this thread Adding an element to page <head> in Orchard CMS, it dawned on me to use the same Handler to insert the CSS link.
PagePartHandler.cs.
using System;
using MyModuleName.Models;
using Orchard.ContentManagement.Handlers;
using Orchard.Core.Title.Models;
using Orchard.Data;
using Orchard.UI.Resources;
using Orchard.Utility.Extensions;
namespace MyModuleName.Handlers {
public class PagePartHandler : ContentHandler {
private readonly IResourceManager _resourceManager;
public PagePartHandler(
IRepository<PagePartRecord> repository,
IResourceManager resourceManager) {
_resourceManager = resourceManager;
Filters.Add(StorageFilter.For(repository));
OnGetDisplayShape<PagePart>(RegisterFestivalProgramStyle);
}
private void RegisterFestivalProgramStyle(BuildDisplayContext context, PagePart part) {
if (context.DisplayType != "Detail")
return;
if (String.IsNullOrWhiteSpace(part.FestivalProgramName))
return;
_resourceManager.RegisterLink(new LinkEntry
{
Rel = "stylesheet",
Type = "text/css",
Href = "/Themes/Bootstrap/Styles/festival-programs/" + part.FestivalProgramName + ".css"
});
}
}
}
This uses the tradition link style, not ResourceManifest.cs, but WORKS!

In MVC, How can I develop something like the old school User Control?

I have a TreeView, wish is used to display some hierarchy. Also, I have some behaviors implemented, such as if the collapsed nodes will be opened when the parent is checked, or if it's lazy loading or will load everything automatically. But I don't want to let these behaviors hard coded. I want to create a customizable control. Something like Telerik Kendo UI does. They have a control developed, that can be used like this:
#(Html.Kendo().MaskedTextBox()
.Name("phone_number")
.Mask("(999) 000-0000")
.Value("555 123 4567")
)
Notice that you can pass some options to it.
Also, I want to be able to pass the name of the action that will populate my TreeView using async. So, if I use this component at, for example, mysite/myController/indexAction, I want to pass a name of the action who will populate my component asynchronous. Let's exemplify. I want something like this:
#(Html.Foo().HierarchyView()
.Name("myTree")
.AllowCheckBoxes(true)
.PopulateChildrenOnCheck(true)
.Async("GetChildrenAsync")
)
So, I can implement at myController an action
string GetChildrenAsync(int idKey) {
var nodes = new List<HierarchViewNodes>();
(...) //implementation to populate the children nodes
return JsonConvert.SerializeObject(nodes);
}
So, it would be an start for my customizable control. Of course I can extend it a lot.
I've searched and learned about RenderPartial and RenderAction, but I can't figure out yet how I can fully use it to make really reusable controls like the one I explained.
You might want to take a look at making some custom HTML helpers. For example, yours might look something like this:
public static MvcHtmlString MyTreeView(this HtmlHelper html, params...){
var myHtmlContent = ....;
// .. implement your functionality and generate the html to put in your view
return MvcHtmlString.Create(myHtmlContent);
}
and you could use it in your Razor view like this:
<div>#Html.MyTreeView(params...)</div>
That is a very simple example, but hopefully it puts you on the right track.
I'd suggest some HTML extensions, as rwisch45 said, but protected (and organized) within a separate "sub-helper", like Kendo and DevExpress:
public static class HtmlExtensions
{
public static static MyCustomHelper CustomHelper(this HtmlHelper htmlHelper)
{
return new MyCustomHelper(htmlHelper);
}
}
public class MyCustomHelper
{
HtmlHelper _HtmlHelper;
public MyCustomHelper(HtmlHelper htmlHelper) { _HtmlHelper = htmlHelper; }
public MvcHtmlString WriteSomethingInteresting(string value)
{ return new MvcHtmlString(value); }
public MyCustomGrid CreateMyGrid(object gridOptions)
{
// I won't show now how to transform dynamic into type.
// You can find that on SO quite easy.
var typedOptions = TransformDynamicIntoClass<GridOptions>(gridOptions);
return new MyCustomGrid(typedOptions);
}
}
public class MyCustomGrid : IHtmlString
{
public string ToHtmlString()
{
// Return your grid as an HTML object!
}
}
This way you'll have:
#Html.CustomHelper().MyCustomGrid(new { Option1 = "", Option2 = "", ...... });
You may, however, play a little with IDisposable, to have something like:
#using(Html.CustomHelper().MyCustomGrid(....))
{
// Dunno what could come here for a grid (poor sample) but maybe for a pane helper?
}

What templating library can be used with Asp .NET MVC?

In my MVC 5 app I need to be able to dynamically construct a list of fully qualified external URL hyperlinks, alone with some additional data, which will come from the Model passed in. I figure - I will need to construct my anchor tags something like this:
{{linkDisplayName}}
with AngularJS this would be natural, but, I have no idea how this is done in MVC.
Is there a templating library that can be used for this?
1) Create a model to Hold the Links
public class LinkObject
{
public string Link { get; set; }
public string Description { get; set; }
}
2) In your Action you can use ViewBag, ViewData or even pass the list inside you Model. I will show you how to do using ViewBag
public ActionResult MyDynamicView()
{
//Other stuff and code here
ViewBag.LinkList = new List<LinkObject>()
{
new LinkObject{ Link ="http://mylink1.com", Description = "Link 1"},
new LinkObject{ Link ="http://mylink2.com", Description = "Link 2"},
new LinkObject{ Link ="http://mylink3.com", Description = "Link 3"}
};
return View(/*pass the model if you have one*/);
}
3) In the View, just use a loop:
#foreach (var item in (List<LinkObject>)ViewBag.LinkList)
{
#item.Description
}
Just create a manual one for that, no need to do it from a template. For example, in javascript
function groupAnchor(url,display){
var a = document.createElement("a");
a.href = url;
a.className = "list-group-item";
a.target = "_blank";
a.innerHTML = display;
return a;
}
And then use that function to modify your html structure
<div id="anchors"></div>
<script>
document.getElementById("anchors").appendChild(groupAnchor("http://google.com","Google"));
</script>
Your approach to modification will more than likely be more advanced than this, but it demonstrates the concept. If you need these values to come from server side then you could always iterate over a set using #foreach() and issue either the whole html or script calls there -- or, pass the set from the server in as json and then use that in a function which is set up to manage a list of anchors.
To expand on this, it is important to avoid sending html to the view from a razor iteration. The reason being that html constructed by razor will increase the size of the page load, and if this is done in a list it can be a significant increase.
In your action, construct the list of links and then serialize them so they can be passed to the view
public ActionResult ViewWithLinks()
{
var vm = new ViewModel();
vm.Links = Json(LinkSource.ToList()).Data;
//or for a very simple test for proof of concept
var Numbers = Json(Enumerable.Range(0,100).ToList()).Data;
ViewData["numbers"] = Numbers ;
return View(vm);
}
where all you need is an object to hold the links in your view model
public class ViewModel
{
public ICollection<Link> Links { get; set; }
}
public class Link
{
public string text { get; set; }
public string href { get; set; }
}
and then in your view you can consume this json object
var allLinks = #Html.Raw(Json.Encode(Model.Links));
var numbersList = #Html.Raw(Json.Encode(ViewData["linkTest"]));//simple example
Now you can return to the above function in order to place it on the page by working with the array of link objects.
var $holder = $("<div>");
for(var i = 0; i < allLinks.length; i++){
$holder.append(groupAnchor(allLinks[i].href,allLinks[i].text));
}
$("#linkArea").append($holder);
The benefit is that all of this javascript can be cached for your page. It is loaded once and is capable of handling large amounts of links without having to worry about sending excessive html to the client.

Avoid to show Null or specific values to razor view engine

I am working on asp.net mvc3 web application using MS Sql server 2008 express rc2. In my app I have two different brands in DB and one of them have few Null or 'unknown' values (e.g. 'unknown' is added to DB instead of Null). My question is how to pass only non null values to View Engine instead of using If/Else statements in View?
in controller:
var model = _data.GetViewModel(query);
if (model != null)
{
return View(model);
}
else
return View("Error");
in ViewModel;
public int Id { get; set; }
public string Query { get; set; }
public string Brand { get; set; }
public string Family { get; set; }
public string Type { get; set; }
in Model:
public ViewModel GetViewModel(string query)
{
var data = _comp.Get(p => p.Query == query);
if (data == null) return null;
return new ViewModel
{
Id = data.id,
Brand = data.brand,
Family = data.family,
Type = data.type
};
}
in View (I am currently using If statement):
#if (Model.Brand != null)
{
<span class="brand">#Model.Brand</span>
}
#if (Model.Family != null)
{
<span class="family">#Model.Family</span>
}
#if (Model.Type != null)
{
<span class="type">#Model.Type</span>
}
Note: I want to avoid If statement because there are too many values in the Database of each brand, and many of the them are Null, So I don't want to generate Html for those Null values. I am using If/Else statement like above code, and for checking too many values in View using If, it costs Memory on server and processor, and it also slow down server response time.
I want to have an alternative method to do this. Should I use Partial views or anything else?
Please Please help me to solve this, Your help is very appreciated.
Thanks and Regards.
First, some background/context, then my suggestion.
(By the way, this all applies to any version of ASP.NET MVC or ASP.NET NancyFX (yes, there's another option out there!!), etc)
Context / Background
To solve this, people generally fall into two types of categories:
Just get data and let the View decide what to show (common one, not the proper way IMO).
The Controller should handle all the heavy lifting and give the view the exact answer (proper way, IMO).
The first way is quick and dirty. Sure it works, but it puts too much logic into the view. Views are not supposed to do any logic at all (exception: for loops, and maybe the odd if/else, maybe). The main reason for this is testing. Yep, that dirty word which people hate and think it's for hippies only. Or .. I don't have the time to test.. so I manually test, etc.. If you put any business logic into a view, you cannot test that.
The second way might seem a bit slower at first, but that's like anything - the more you practice, the faster you go. This is (IMO) the preferred method of doing things because you can test the controller. The controller should create a view model which will have -the exact- results that the view needs. Not extra. For example, imagine you want to return a list of Brands to the display/view. Most people do (the equivalent of) Get-all-brands into a list, and send that list to the view, even though 80% of those properties are -not- going to be used by that view! Even if ONE property is not going to be used by that view, do not retrieve it nor send it to the view!
So - TL;DR; do all the heavy lifting in the controller. The View is dumb. Just dump the exact view model data, to the view.
Solution to your problem
Ok, so let's roll with idea #2 and get all this stuff happening in the controller.
// Grab the results.
// ASSUMPTION: It is only returning the -exact- data I need. No more, no less.
var results = _data.GetViewModel(query);
if (model == null)
{
// Project the results into a perfectly tight & svelte view model
// 100% specific for this view.
var viewModel = results.
Select(x => new ViewModel
{
Id = x.Id,
Brand = string.IsNullOrEmpty(x.Brand)
? string.Empty
: x.Brand,
Family = string.IsNullOrEmpty(x.Family)
? string.Empty
: x.Family,
Type = string.IsNullOrEmpty(x.Type)
? string.Empty
: x.Type,
}).ToList();
return viewModel;
Testing this..
[Fact]
public void GivenSomeBrands_Index_ReturnsAViewModel()
{
// Arrange.
// NOTE: Our fake repostitory has some fake data. In it ..
// Id: 1, Brand: Gucci.
// Id: 22, Brand: null.
var controller = new BrandController(SomeFakeRepositoryThingy);
// Act.
var result = controller.Index(); // This calls that controller code, above.
// Assert.
Assert.IsNotNull(result); // Controller returned some result.
Assert.IsNotNull(result.Model); // We have some model data.
var model = result.Model as IList<ViewModel>(); // Cast the Model value.
Assert.NotNull(model); // We have a strongly typed view model.
// We check the first brand value.
Assert.Equal("Gucci", model.First().Brand);
// We know this item has a null Brand,
Assert.Equal(string.Empty, model[21].Brand); but the ViewModel converted it.
}
You could write a custom HTML helper:
public static string MyHelper<V>(this HtmlHelper helper, V value, string css)
{
if (value == null)
return "";
return String.Format("<span class='{0}'>{1}</span>", value, css);
}
Then in your view:
#Html.MyHelper(Model.Brand, "brand");
#Html.MyHelper(Model.Family, "family");
#Html.MyHelper(Model.Type, "type");

Determine the model of a partial view from the controller within MVC

My current problem is that I have a partial view that I want to determine what model is being used by it.
I have had to deal with a few strange scenarios for my project so I will try to outline it here, maybe someone can offer a better way to do this.
I am designing something like the Google iGoogle page. A main page with multiple widgets that are able to move around or be configured as needed. The current system loads the actual widget's data asynchronously view a POST to a controller within my application. That controller will either render a partial view to HTML that can be returned (and then loaded into the page view JQUERY) or just straight HTML/JavaScript that is stored in a database.
This was working fine for me, I had a model for the widgets that holds a dictionary of options that are described via the database, and then used by the partial view. The problem came when I wanted to pass data to a partial view. The best solution I could come up with was having the controller determine which model the partial view in question uses, have some function that will fill the model, and then pass it, along with the partial view, to the function that will render it to HTML within the controller.
I realize this is an odd scenario for MVC (the layers are blending...) and any advice on fundamental design, or implementation of this would be greatly appreciated.
I am currently using MVC3/Razor. Feel free to ask any other questions.
I prototyped a possible solution to this, because it seemed like a fun problem. I hope it's useful to you.
Models
First, the models. I decided to create two 'widgets', one for news, and one for a clock.
public class NewsModel
{
public string[] Headlines { get; set; }
public NewsModel(params string[] headlines)
{
Headlines = headlines;
}
}
public class ClockModel
{
public DateTime Now { get; set; }
public ClockModel(DateTime now)
{
Now = now;
}
}
Controller
My controller doesn't know anything about the views. What it does is returns a single model, but that model has the ability to dynamically fetch the right model as required by the view.
public ActionResult Show(string widgetName)
{
var selector = new ModelSelector();
selector.WhenRendering<ClockModel>(() => new ClockModel(DateTime.Now));
selector.WhenRendering<NewsModel>(() => new NewsModel("Headline 1", "Headline 2", "Headline 3"));
return PartialView(widgetName, selector);
}
Delegates are used so that the correct model is only created/fetched if it is actually used.
ModelSelector
The ModelSelector that the controller uses is pretty simple - it just keeps a bag of delegates to create each model type:
public class ModelSelector
{
private readonly Dictionary<Type, Func<object>> modelLookup = new Dictionary<Type, Func<object>>();
public void WhenRendering<T>(Func<object> getter)
{
modelLookup.Add(typeof(T), getter);
}
public object GetModel(Type modelType)
{
if (!modelLookup.ContainsKey(modelType))
{
throw new KeyNotFoundException(string.Format("A provider for the model type '{0}' was not provided", modelType.FullName));
}
return modelLookup[modelType]();
}
}
The Views - Simple solution
Now, the easiest way to implement a view would be:
#model MvcApplication2.ModelSelector
#using MvcApplication2.Models
#{
var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}
<h2>The time is: #clock.Now</h2>
You could end here and use this approach.
The Views - Better solution
That's pretty ugly. I wanted my views to look like this:
#model MvcApplication2.Models.ClockModel
<h2>Clock</h2>
#Model.Now
And
#model MvcApplication2.Models.NewsModel
<h2>News Widget</h2>
#foreach (var headline in Model.Headlines)
{
<h3>#headline</h3>
}
To make this work, I had to create a custom view engine.
Custom view engine
When a Razor view is compiled, it inherits a ViewPage<T>, where T is the #model. So we can use reflection to figure out what type the view wanted, and select it.
public class ModelSelectorEnabledRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var result = base.CreateView(controllerContext, viewPath, masterPath);
if (result == null)
return null;
return new CustomRazorView((RazorView) result);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var result = base.CreatePartialView(controllerContext, partialPath);
if (result == null)
return null;
return new CustomRazorView((RazorView)result);
}
public class CustomRazorView : IView
{
private readonly RazorView view;
public CustomRazorView(RazorView view)
{
this.view = view;
}
public void Render(ViewContext viewContext, TextWriter writer)
{
var modelSelector = viewContext.ViewData.Model as ModelSelector;
if (modelSelector == null)
{
// This is not a widget, so fall back to stock-standard MVC/Razor rendering
view.Render(viewContext, writer);
return;
}
// We need to work out what #model is on the view, so that we can pass the correct model to it.
// We can do this by using reflection over the compiled views, since Razor views implement a
// ViewPage<T>, where T is the #model value.
var compiledViewType = BuildManager.GetCompiledType(view.ViewPath);
var baseType = compiledViewType.BaseType;
if (baseType == null || !baseType.IsGenericType)
{
throw new Exception(string.Format("When the view '{0}' was compiled, the resulting type was '{1}', with base type '{2}'. I expected a base type with a single generic argument; I don't know how to handle this type.", view.ViewPath, compiledViewType, baseType));
}
// This will be the value of #model
var modelType = baseType.GetGenericArguments()[0];
if (modelType == typeof(object))
{
// When no #model is set, the result is a ViewPage<object>
throw new Exception(string.Format("The view '{0}' needs to include the #model directive to specify the model type. Did you forget to include an #model line?", view.ViewPath));
}
var model = modelSelector.GetModel(modelType);
// Switch the current model from the ModelSelector to the value of #model
viewContext.ViewData.Model = model;
view.Render(viewContext, writer);
}
}
}
The view engine is registered by putting this in Global.asax.cs:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());
Rendering
My home view includes the following lines to test it all out:
#Html.Action("Show", "Widget", new { widgetName = "Clock" })
#Html.Action("Show", "Widget", new { widgetName = "News" })
One option would be to extend the idea of partial requests in your application. Steve Sanderson has a fantastic example of this, although the post relates to MVC 1 & 2. I think it would still help in you v3, but I haven't investigated v3 to see if the MVC team implemented their own version. In your asynch scenario, you'll need to toy with the implementation a bit, perhaps change the PartialRequest definition to accept different information as needed, but I think this might be a good start. The net result would be better isolation of concerns, allowing individual controllers to manage a particular type of partial, and in turn be better aware of the model Type you want to work with.
I'm not 100% sure that this is what you'd be looking for, but the [ChildActionOnly] attribute can be added to a method within your controller. That requires that the method can only be called from a partial view. Then you can set up your partial view for that method that basically resembles one of your widgets. Check out the MVC Music Store example here:
http://www.asp.net/mvc/tutorials/mvc-music-store-part-10
What about a dynamic view model? Layouts in MVC3 use them, and maybe you can use something similar for your purposes:
Dynamic in C# 4.0: Introducing the ExpandoObject
Fun With Method Missing and C# 4
Dynamic View Page, MVC without a View Model
I blogged about doing exactly this. Please see http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget
Essentially I built out a similar widget system. The posts also cover how to handle configuration of those widgets. This makes use of the dynamic support in Mvc3 so that any model object can be passed to the view, from a single controller action.
By default all widgets have a collection of KVP properties (I believe this is what the OP has). So for a simple widget we get access to those properties from within the view. I used for a widget that displayed some html (where the html was stored in one of those properties).
However, for more complex widgets we implement IWidgetWithDisplayModel. This tells us that before we pass the loaded widget back to the view, we need to "build" our display model.
Here's the controller action that does that. Check the posts for full details.
[HttpGet]
public ActionResult Get(string name)
{
var widget = widgetService.GetWidgetBySystemName(name, true);
if (widget == null)
return Content(string.Format("Widget [{0}] not found!", name));
if (!this.ViewExists(widget.WidgetName))
return Content(string.Format("A template for widget [{0}] was not found.", widget.WidgetName));
if (widget is IWidgetWithDisplayModel) {
(widget as IWidgetWithDisplayModel).CreateDisplayModel();
}
return PartialView(widget.WidgetName, widget);
}

Resources