Razor (.cshtml) as C1 Functions

Topics: General, Release notes
Oct 18, 2011 at 6:13 PM
Edited Oct 19, 2011 at 9:29 AM

We all wanted this, c'mon, don't be afraid to put your hands up in the air. Using Razor syntax in C1 functions without the hazzle of going trough MVC. If you're actually using MVC, like real MVC and not just the Razor syntax, well, then this is not for you and you should just keep using the MVC Player.

Requirement

You need to have Microsoft Webpages installed for this to work. You don't need MVC, since MVC is basically just building upon the WebPages framework. You can get it from here http://www.microsoft.com/download/en/details.aspx?id=15979. If you don't have it installed on the server, the following files should be present in the ~/Bin folder before installing the package

  • Microsoft.Web.Infrastructure.dll
  • System.Web.Razor.dll
  • System.Web.WebPages.Deployment.dll
  • System.Web.WebPages.dll
  • System.Web.WebPages.Razor.dll

How to use

So, how does it work? After you installed the package, you should put your cshtml-files in a new subfolder under ~/App_Data/Razor. The folderstructure is the same as for ~/App_Data/Xslt, so folders become namespaces and filenames is the function name. Functions are not created inside C1 yet, so just put the files here and they will show up in the Function Selector Dialog inside the C1 console.

All cshtml-files will inherit from a common CompositeC1Contrib.RazorFunctions.CompositeC1WebPage, which in turn inherits from System.Web.WebPages.WebPage. This means that you get access to the normal helper methods and properties that you're used to like Html, Context, Layout, Page, PageData etc. But notice, its NOT the same as a MvcWebPage so we don't have the notion of Model or Controllers. Its a pure templating engine without any logic. You also have access all the usual .Net stuff of course, and from CompositeC1WebPage you have access to Data and CurrentPageNode. I will add much more later, but this should get you started.

Examples

Here is an example of using an Extension Method and traversing the Sitemap to create a breadcrumb. It consists of two files, a .cs that you can put in App_Code and a .cshtml file that you'll put in ~/App_Data/Razor

namespace Composite.Data
{
    public static class SitemapNavigatorExtensions
    {
        public static IEnumerable<PageNode> GetOpenPagesPath(this PageNode currentPage)
        {
            var openPages = new List<PageNode>();
            var openPage = currentPage;

            while (openPage != null)
            {
                openPages.Add(openPage);

                openPage = openPage.ParentPage; // crawl up
            }

            return openPages;
        }       
    }
}
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Testing razor function</title>
  </head>

  <body>
    <ul id="Breadcrumb">
      @foreach  (PageNode page in @CurrentPageNode.GetOpenPagesPath().OrderBy(op => op.Level))
      {
      <li>
        <a href="@(page.Url)" >@(String.IsNullOrWhiteSpace(page.MenuTitle) ? page.Title : page.MenuTitle)</a>
      </li>
      }
    </ul>
  </body>  
</html>

Another example is how you would query data from the DataLayer and print it out directly with .cshtml. This only requires a file in ~/App_Data/Razor

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <link rel="stylesheet" type="text/css" media="screen" href="~/Frontend/Styles/Functions/News.css"/>
  </head>

	<body>
		<h1>Latest news</h1>

		<ul id="news">
      @foreach  (var newsItem in @Data.Get<Omnicorp.Content.News>().OrderByDescending(n => n.Date))
      {
		<li>
			<a title="Read more" href="/Renderers/Page.aspx?pageId=398932b1-314b-4baf-b3f0-b1445f806aae&amp;story=@newsItem.Date.Year.@newsItem.Date.Month.@newsItem.Date.Day:@HttpUtility.UrlEncodeUnicode(newsItem.Title)">
				<span>
					@newsItem.Date.Year.@newsItem.Date.Month.@newsItem.Date.Day:
				</span>
				<strong>
					@newsItem.Title
				</strong>
				<br/>
				<span>
					@newsItem.Summary
				</span>
			</a>
		</li>
      }
    </ul>
  </body>
</html>

Known issues

  • Except for the so far limited access to helper methods, properties etc. at runtime, there is none.

Download the package from here http://compositec1contrib.codeplex.com/SourceControl/list/changesets and unzip. In C1 Console go to install local packages, and browse for RazorFunctions/Package/package.zip

Coordinator
Oct 18, 2011 at 7:14 PM

@burningice, this is really sweet and I like the simplicity of just having the .cshtml file and no meta data handing around - but getting parameters in there is a must. Any thoughts on how this could be done?

It could be done using helpers?

 

@helper Repeat(int count) {
  for( int i = 0; i<count; i++ ) {
    <div>@count</div>
  }
}

This could be picked up as function "Repeat" and file name could be ignored, so folders still define name space? Meta data that define labels, widgets and default/test values could be added, kind of like we do with C# functions.

I'm thinking out loud, but we'd definitely need parameters here - imagine a function like the one below, and I'd personally be spending a lot less time using Web Forms and XSLT. 

@helper LatestNewsList(Exprission<Func<Omnicorp.Content.News,bool>> newsFilter) {
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <link rel="stylesheet" type="text/css" media="screen" href="~/Frontend/Styles/Functions/News.css"/>
  </head>

	<body>
		<h1>Latest news</h1>

		<ul id="news">
      @foreach  (var newsItem in @Data.Get<Omnicorp.Content.News>().Where(newsFilter).OrderByDescending(n => n.Date))
      {
		<li>
			<a title="Read more" href="/Renderers/Page.aspx?pageId=398932b1-314b-4baf-b3f0-b1445f806aae&amp;story=@newsItem.Date.Year.@newsItem.Date.Month.@newsItem.Date.Day:@HttpUtility.UrlEncodeUnicode(newsItem.Title)">
				<span>
					@newsItem.Date.Year.@newsItem.Date.Month.@newsItem.Date.Day:
				</span>
				<strong>
					@newsItem.Title
				</strong>
				<br/>
				<span>
					@newsItem.Summary
				</span>
			</a>
		</li>
      }
    </ul>
  </body>
</html>
}

 

Oct 18, 2011 at 7:57 PM
Edited Oct 18, 2011 at 7:59 PM

It could might work like that... doing the compilation of cshtml files at system startup and with some reflection emitting the correct ParameterProfiles. hm, clever actually, i need to look into that.

Otherwise just going the Xslt-functions way was  what i had in mind, defining input parameters etc. i the console, putting the input data in the PageData-property so they can be accessed from Razor (http://msdn.microsoft.com/en-us/library/system.web.webpages.webpagebase.pagedata(v=vs.99).aspx)

Oct 19, 2011 at 12:47 PM
Edited Oct 20, 2011 at 1:18 PM

So, now we actual got a way to auto-populate function parameters for our Razor functions, without having to going trough the console as we do it for XSLT functions. The way we do it it by defining fields or properties and decorate them with the FunctionParameter attribute. This way they will automatically be populated and can be used in our Razor Function.

A example is our previous news-list function, that would now look like this

@functions {
    [FunctionParameter("Page filter", "Help", null)]
    private Expression<Func<IPage, bool>> pageFilter;
}

@{
    var pages = @Data.Get<IPage>();
    if (pageFilter != null)
    {
        pages = pages.Where(pageFilter);
    }    
}

<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<link rel="stylesheet" type="text/css" media="screen" href="~/Frontend/Styles/Functions/News.css"/>
	</head>
	
	<body>
		<h1>Latest news</h1>
	
		<ul id="news">
			@foreach  (var page in @pages) 
                            {
			<li>
				<a title="Read more" href="/Renderers/Page.aspx?pageId=@page.Id">
					<span>@page.Title</span>
				</a>
			</li>
			}
		</ul>
	</body>
</html>

You can also use custom types and put in multiple parameters, no problem.

@functions {
    [FunctionParameter("News filter", "Help", null)] 
    private Expression<Func<Omnicorp.Content.News, bool>> newsFilter;

    [FunctionParameter("Count", "Help", 10)]
    private int count;
}
 
@{
	var news = @Data.Get<Omnicorp.Content.News>();
	if (newsFilter != null) 
	{
		news = news.Where(newsFilter);
	}

	news = news.Take(count);
}
	
<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<link rel="stylesheet" type="text/css" media="screen" href="~/Frontend/Styles/Functions/News.css"/>
	</head>
	
	<body>
		<h1>Latest news (showing @news.Count() items)</h1>
	
		<ul id="news">
			@foreach  (var newsItem in @news) 
			{
				<li>
					<a title="Read more" href="/Renderers/Page.aspx?pageId=@newsItem.Id">
						<span>@newsItem.Title</span>
					</a>
				</li>
			}
		</ul>
	</body>
</html>

kinda like @mawtex suggested.. and its even possible to omit the main-helper if you don't need any input parameters for your function like this

@{
	var newsItem = @Data.Get<Omnicorp.Content.News>().OrderBy(n => n.Date).First();
}

<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<link rel="stylesheet" type="text/css" media="screen" href="~/Frontend/Styles/Functions/News.css"/>
	</head>
	
	<body>
		<h1>Latest newsitem</h1>
	
		<ul id="news">
			<li>
				<a title="Read more" href="/Renderers/Page.aspx?pageId=@newsItem.Id">
					<span>@newsItem.Title</span>
				</a>
			</li>
		</ul>
	</body>
</html>

You can even omit the html and head and body tags if you want

@{
	var newsItem = @Data.Get<Omnicorp.Content.News>().OrderBy(n => n.Date).First();
}

<h1>Latest newsitem</h1>
	
<ul id="news">
	<li>
		<a title="Read more" href="/Renderers/Page.aspx?pageId=@newsItem.Id">
			<span>@newsItem.Title</span>
		</a>
	</li>
</ul>
Oct 20, 2011 at 12:06 PM
Edited Oct 20, 2011 at 12:14 PM

Finally satisfied with how Function Parameters are defined and consumed. Updated sample code in this thread.

Also included four (working) example Razor Functions for inspiration.

Breadcrumb

 

@functions {
    private IEnumerable<PageNode> OpenPages(PageNode selectedPageNode)
    {
        var openPages = new List<PageNode>();
        var openPage = selectedPageNode;

        while (openPage != null)
        {
            openPages.Add(openPage);
            openPage = openPage.ParentPage; // crawl up
        }

        return openPages;
    }
}

<ul id="Breadcrumb">
    @foreach (var page in OpenPages(@CurrentPageNode).OrderBy(op => op.Level))
    {
        <li>
            <a href="@(page.Url)" >@(String.IsNullOrWhiteSpace(page.MenuTitle) ? page.Title : page.MenuTitle)</a>
        </li>
    }
</ul>

Sitemap

 

<div id="Sitemap">
    @NavigationTree(@HomePageNode.ChildPages, 5)
</div>

@helper NavigationTree(IEnumerable<PageNode> pages, int endRenderLevel)
{
    if (pages.Any() && pages.First().Level <= endRenderLevel)
    {
        <ul class ="sitemapLevel@(pages.First().Level)">
            @foreach  (var subPage in pages)
            {
                if  (!String.IsNullOrWhiteSpace(subPage.MenuTitle))
                {
                    <li>
                        <a href="@(subPage.Url)">@subPage.MenuTitle</a>
                        @NavigationTree(subPage.ChildPages, endRenderLevel)
                    </li>
                }
            }
        </ul>
    }
}

Subnavigation

 

@functions {
    private IEnumerable<PageNode> OpenPages(PageNode selectedPageNode)
    {
        var openPages = new List<PageNode>();
        var openPage = selectedPageNode;

        while (openPage != null)
        {
            openPages.Add(openPage);
            openPage = openPage.ParentPage; // crawl up
        }

        return openPages;
    }
}

<div id="Subnavigation">
    @if (OpenPages(@CurrentPageNode).Where(p => p.Level == 2).Any())
    {
        var openLevel2Page = OpenPages(@CurrentPageNode).Where(p => p.Level == 2).First();
        <h1>
            <a href="@(openLevel2Page.Url)">@(openLevel2Page.MenuTitle)</a>
        </h1>
        @NavigationTree(openLevel2Page.ChildPages, 5)
    }
</div>

@helper NavigationTree(IEnumerable<PageNode> pages, int endRenderLevel)
{
    if (pages.Any() && pages.First().Level <= endRenderLevel)
    {
        <ul class ="subnavigationLevel@(pages.First().Level)">
            @foreach (var subPage in pages)
            {
                if (!String.IsNullOrWhiteSpace(subPage.MenuTitle))
                {
                    var isOpen = OpenPages(@CurrentPageNode).Any(op => op.Id == subPage.Id);
                    var isSelected = @CurrentPageNode.Id == subPage.Id;
                        
                    <li>
                        <a href="@(subPage.Url)" class ="@(isOpen ? " open" : " closed") @(isSelected ? " selected" : " ")">
                            @subPage.MenuTitle
                        </a>
                        @if (isOpen)
                        {
                            @NavigationTree(subPage.ChildPages, endRenderLevel);
                        }
                    </li>
                }
            }
        </ul>
    }
}

Topnavigation

 

 

<ul id="Topnavigation">
    @foreach (var page in @HomePageNode.ChildPages.Where(tp => !String .IsNullOrWhiteSpace(tp.MenuTitle)))
    {
        <li class ="@SelectedClass(page)">
            <a href="@(page.Url)" >@(String.IsNullOrWhiteSpace(page.MenuTitle) ? page.Title : page.MenuTitle)</a>
        </li>
    }
</ul>

@helper SelectedClass(PageNode page)
{
    if (page.Id == @CurrentPageNode.Id)
    {
        <text>selected</text>
    }
}