. Figure 2-9. The new contact page form Contact Page | 29 Home Page For the home page, we’ll start by making a couple of minor content changes. Under “Content” we’ll click on the listing currently titled “Welcome to Orchard!” and update its title to “Daisy’s Online.” Clicking “Publish Now” makes these changes available on the live site. Note that because this page has been set to the home page by way of the “Set as home page” field, we can get to this page at the root site URL http://localhost/ orchard. Figure 2-10. The Daisy’s Gone contact page 30 | Chapter 2: Creating and Managing Content Zones and Layers At this point, our home page hasn’t been customized much beyond the small tweaks we made in the previous chapter. To add additional customization, we’re now going to add a couple of new modules from the Gallery that we’ll then add to the TripelSecond zone. We want to give visitors the ability to “Like” the band on Facebook right from the home page. Searching for “Facebook” in the Gallery yields more than a handful of results. We’re going to install the Facebook.Like module by David Fekke. After installing and enabling this module, we’ll add it to the home page. We’ll also add a Twitter follow button by installing the Twitter Follow Button module by Nicholas Mayne (search for “twitter”). To add the Facebook widget to the home page, click over to “Widgets” and set the current layer to “TheHomepage.” Remove the “Second Leader Aside” widget from the TripelSecond zone. Then click “Add” on “TripelSecond” and select the Facebook Like widget. In the form for this widget, enter the title “Like Daisy’s Gone on Face- book.” Leave the other defaults and save. Repeat these steps to add the Twitter Follow widget also to the TripelSecond zone. Use the title “Follow” and provide the Twitter username “@daisysgone”. After saving both widgets, refresh the home page to see that both widgets have been added. Figure 2-11. Social widgets on the home page Zones and Layers | 31 Clicking to other pages on the site, you’ll notice that these widgets don’t appear any- where but the home page. The reason for this behavior is that the rule that determines whether to display these widgets is defined in the layer “TheHomepage.” Clicking on “Widgets,” selecting the layer “TheHomepage,” and clicking “Edit” displays a form for editing the layer. The “Layer Rule” field defines a Boolean expression that determines whether a widget is displayed. “TheHomepage” has a rule url '~/’ that is true when the URL is the root site URL. Layer rules have a terse syntax, using the logical operators not, and, and or along with the url and authenticated functions. To demonstrate how to use layers, we’re going to add a new Projection widget to the zone AsideSecond on all pages except the events page. On the Widgets admin page, you probably noticed the image that shows where zones are placed in the layout. Note that this image was created by the theme designer and will only be accurate if the CSS rules render zones in a way that matches the zones as shown in the image. If you modify the theme’s CSS rules, there’s no guarantee this image will re- main accurate. Click Widgets→Add a new layer. Name this layer “NotTheEventsPage”. Add a rule not url '~/events’ and then save. After saving, Orchard returns to the “Widgets” admin page with the new layer selected. Click “Add” on AsideSecond and select “Projection Widget.” Enter a title of “Upcoming Events.” Select the query “All Events (1 columns grid)”, and limit the number of items to display to 3. After saving and refreshing the site, we can see that the new events projection widget appears on all pages except for the events page. Gallery For the gallery page, we’re going to install and use the Image Gallery module by Gabriel Eduardo Chites de Mello. After installing and enabling this module, a new admin menu option is added. Click Image Gallery→Add new Image Gallery. Name the new gallery “Gallery Page.” After saving, click the new gallery’s name in the listing and then upload new images by clicking “Add Images” and using the upload form. Again, click Widgets→Add New Layer... and name that layer “Gallery.” Set the rule to url '~/gallery’ so that we can add the Image Gallery widget to a page at this URL only. After saving the layer add the Image Gallery widget to the AfterContent zone. Leave the defaults, other than the required title. 32 | Chapter 2: Creating and Managing Content We’ll next create the actual gallery page via New→Page. Add some content, add the title “Daisy’s Gallery,” set the permalink to “gallery” to match our rule, add the page to the main menu with the text “Gallery,” and publish. After publishing, you can browse to this page by clicking the “Gallery” tab on the main menu (Figure 2-13). I originally planned to name the “Gallery” page “Media.” However, Media is already a special directory in Orchard and the URL route col- lided resulting in a 404 Not Found error by IIS. Figure 2-12. The new NotTheEvents layer Gallery | 33 Figure 2-13. The Image Gallery Summary At this point, we’ve touched upon most of the core content management features of Orchard. We’ve seen how easy it can be to create new content types. We’ve also seen how we can augment existing types. In the next chapter, we’re going to look at cus- tomizing how the content we create is displayed by Orchard. 34 | Chapter 2: Creating and Managing Content CHAPTER 3 Displaying Content As we saw in the previous chapter, getting a basic Orchard site up and running is a pretty low friction process. Without having to write a single line of code, we were able to create new pages with dynamic content and reasonably advanced functionality. However, we also saw that the content we displayed has a default rendering, which we can’t fully customize through the admin tools. Fortunately, Orchard does provide functionality to allow us to alter this rendering. In fact, with minimal effort Orchard gives us great control over how we display content. Virtually every piece of data that gets rendered by Orchard (collectively known as shapes) may be customized by overriding its default template. In this chapter, we’ll learn about the conventions and tools that make this process relatively straightforward. Customizing Biography Content As it exists now, the bio projection page simply takes each field in our Bio content type and renders its label and value one after the other with each on its own line (see Fig- ure 3-1). It would be better if our content read a little more naturally, such as “John Zablocki was born in Wethersfield, Connecticut, and plays guitar for Daisy’s Gone.” The label-value rendering might be sufficient for a product listing, but is less suited for our biography listing. Ultimately, we’ll create a template for rendering Bio content items in a projection page (or any other container). However, to demonstrate how Orchard alternates work, we’ll start at a higher level and change the way all content is displayed when it’s rendered in a container (a projection in our case). Orchard content is rendered differently based on its usage. For example, when content items such as those created from our Bio type are added to a projection page, their summary display type is used. When a full content item is rendered (i.e., an individual biography), then the detail display type is used. 35 Content Templates If you open up the Themes project in your Orchard solution (in the “Themes” solution folder) you’ll find a directory named TheThemeMachine, which is the default Orchard theme. The styles associated with this theme are kept intentionally simple, because this theme is generally used as a starting point for building new themes. In the next chapter, we’ll create a new theme from scratch, but for now we’re going to work within this one. The first thing to do is expand the Views directory. There you’ll see six Razor (.cshtml) files already in this folder. These files are all used by the “TheThemeMachine” theme. We won’t explore these files now as we’ll discuss them in the next chapter. Add a file to this directory, named Content.Summary.cshtml. If you clear the contents of this new Razor file, save it and then refresh the “Bios” projection page, what you’ll see (Figure 3-2) is that the Bio item summaries have disappeared. Also notice that the “Upcoming Events” are no longer appearing in the projection widget that we added to the AsideSecond zone. Figure 3-1. The default bio projection page 36 | Chapter 3: Displaying Content The Themes project was not created using an ASP.NET MVC Visual Studio project template, therefore the usual MVC context menu short- cuts are absent from this project (i.e., “Add View”). Instead, I use Add→New Item and select “HTML Page” from the list of web templates. This action requires manually setting the .cshtml extension. As you might suspect, Orchard found our new alternate template and used that file to render the summary display for our Bio items. You might also have realized that like our “Upcoming Events,” the Event and Blog Post items have also disappeared. You can verify this by clicking on the “Events” or “Blog” tabs. What this change is starting to demonstrate is how Orchard performs the task of “shape rendering.” Figure 3-2. The effects of a blank Content.Summary.cshtml file Alternate Templates There are several file and directory naming conventions used by Orchard to determine which template should be used to render a particular piece of content. In our example, creating a view named Content.Summary instructed Orchard to use this template for all content shapes rendered with the summary display type. These views are known as “alternate templates.” Again, content items in a projection page are rendered using the “summary” display type. We could edit our alternate template to add the header (title) and content (ev- erything else) back in. To do so, we need to add only two lines of code. For illustrative purposes only, we’ll also change the background color of the content:
@Display(Model.Header) @Display(Model.Content)
If you save the template and refresh the bio, event, and blog projection pages, you’ll see that the content is again being rendered as before, but now with a gray background. You’ll also see our “Upcoming Events” widget being rendered the same way. Customizing Biography Content | 37 To change this alternate template so that only Bio content items are affected by the customization, you simply have to rename the file to Content-Bio.Summary.cshtml. Orchard will parse the token after the hyphen and match it to a content type id. In this case “Bio” was the default content type id associated with the Bio content type when we created it. With this change, Event and Blog Post items are no longer rendered with a gray background within their respective containers. You could also rename the file to Content-17.Summary.cshtml, where “17” is the unique identifier of a particular piece of content (my bio in my case). After renaming the file, if you refresh the “Bios” page you’ll see that only the bio summary specified in the filename has been grayed (Figure 3-3). This item level customization might be useful for calling out a particular item in a list, such as a special event. Figure 3-3. A template for a single content item Let’s rename our alternate template back to Content-Bio.Summary.cshtml so that we can return to our original goal, which was to render our bio listings with more natural language. To achieve this goal, we’re going to create a simple template that will display values from our content type in an order that we define. 38 | Chapter 3: Displaying Content Our template first defines a simple Razor function splitName for convenience. We’re going to take the shortcut of parsing our first name out of the title, as opposed to creating a separate field for this value in our type definition. We’ll then check whether a profile picture has been set. If it has, we’ll display it: @{ Func splitName = (name) => name.Split(' '); }
@{ var fileName = Model.ContentItem.Bio.Headshot.FileName; if (! string.IsNullOrEmpty(@fileName)) { } } @Model.ContentItem.TitlePart.Title plays @Model.ContentItem.Bio.Instruments.Value.ToLower() for Daisy's Gone. @splitName(Model.ContentItem.TitlePart.Title)[0] is originally from @Model.ContentItem.Bio.Birthplace.Value and currently lives in @Model.ContentItem.Bio.LivesIn.Value.
Next we display a link to the full bio page. We set the href property of the anchor tag by accessing the Bio content item’s Autoroute content part, which has the URL slug for permalinks. We then construct a sentence that summarizes a little information about each musician. In this sentence, we’ll access both the Bio type’s fields and its Title part to fill in the template values. The important thing to note is that if you want to reference fields from a content item in your templates, you’ll need to use a dynamic expression that is generally of the form Model.ContentItem.{ContentType}.{FieldName}.Value. Other elements such as the title require first accessing its content part. Later on in this chapter, we’ll see how to discover these values using the Orchard tools. We just fixed our bio summary templates so that the content wouldn’t just dump out labels and values for each property. However, when we click through to an individual bio, we can see that the detail view for our page looks like our summary did before our customization. To get our bio details and summary views more in line, we’ll add a new alternate tem- plate. Start by creating a new Razor file named Content-Bio.cshtml in the Views directory in “TheThemeMachine.” This new template will be only a slight variation of the Bio item summary template we just created:
@Model.ContentItem.TitlePart.Title
@{ var fileName = Model.ContentItem.Bio.Headshot.FileName; if (!string.IsNullOrEmpty(fileName)) { Customizing Biography Content | 39 } } Plays @Model.ContentItem.Bio.Instruments.Value.ToLower() for Daisy's Gone. Originally from @Display(Model.ContentItem.Bio.Birthplace.Value). Currently lives in @Model.ContentItem.Bio.LivesIn.Value. @Html.Raw(Model.ContentItem.BodyPart.Text) Figure 3-4. The customized bio listing It’s also worth noting that if we removed the summary alternate template, this new template would then be used to render summary listings as well since the name of this file doesn’t specify a display type (i.e., Summary or Detail). We could also have (and probably should have) named this file Content-Bio.Detail.cshtml to remove the poten- tial impact on summary displays. Since shapes are dynamic types, it’s occasionally helpful to attach the Visual Studio debugger to your Orchard application to inspect the ob- ject being bound to your view. You can set a break point in a Razor file in a code block, which is any executable line between a @{ }. 40 | Chapter 3: Displaying Content Customizing Events Like our bios in our bio listing, events in our events listing are rendered one property at a time with labels and values occupying one line each. Arguably, this display is ap- propriate for events, so we won’t create an alternate template for event summaries. However, we do want to adjust the order in which fields are rendering. Figure 3-5 shows that the location appears before the body field, while the event date is after. We’ll move both of these fields ahead of the HTML body field. Figure 3-5. Default event listing Placement Files To rearrange the fields, we’ll use a special XML file named Placement.info that lives in the root of a template directory. This file defines rules for rendering content items, parts, and fields. “Place rules” allow module developers and template designers to pro- vide preferred layout ordering for content pieces. We’ll modify the Placement.info file in the root directory of the “TheThemeMachine” theme by replacing its content entirely with this XML: Customizing Biography Content | 41 The Match element will limit the placement rule so that it affects only items of type Event that are shown with the summary display type. The Place rules that follow the Match specify that the text and date time fields will appear before the Content zone, where the HTML body is placed. If we want to force the event date to render after the location, we could provide relative numeric values instead of “before” or “after”: Field Templates We also want to jazz up our location field so that it renders a link to Bing Maps for that particular location. To do so, we’ll create an alternate template for just that field. This is different from what we did with Bio content, where the alternate template affected an entire item’s rendering. Create a new directory named Fields under the Views directory in the “TheThemeMa- chine” theme. To that new directory, add a file named Common.Text-Event-Loca- tion.cshtml (prefixing this file with Fields would make the new directory unnecessary). The body of this new template will simply supply the value of our event’s location to the “q” parameter used by Bing Maps when it performs a search: @Model.Name: @Model.Value If you refresh the event listing, you’ll now see the location field is wrapped in a link to Bing Maps. If you click through to view the actual event, you’ll see that the location field is also linking to Bing Maps (Figure 3-6). Our field alternate applies to both sum- mary and detail displays. There is still one last problem we want to fix. The ordering of the event fields in the details display type is not the way we want it. The event date is rendering below the content as was previously the case with event summary displays (Figure 3-6). To fix this, we’ll simply remove the Match tag from Placement.info where we’d previously re- stricted the display rule to summary only: 42 | Chapter 3: Displaying Content Shape Tracing While for some shapes it’s easy enough to figure out the correct naming for alternate templates based on the documented rules, it’s not always obvious. Fortunately, there’s the Shape Tracing module to help navigate the complex hierarchy of shapes that com- pose a page. Shape Tracing is installed but disabled when you create a site with the “Default” recipe. It can be enabled on the admin dashboard under Modules→Features. It’s listed under the “Designer” category. After you enable the Shape Tracing module, across the bottom of each page of your site you’ll see a small bar with a small square icon all the way to the right. Clicking it will bring up the tools for outlining the shapes on a page. Shape Tracing is a JavaScript-based utility that provides information about how zones and the shapes contained in these zones are rendered. As you move your mouse around the page, the Shape Tracing tool highlights content pieces such as zones, widgets, con- tent parts, and fields. Figure 3-6. The event location linking to Bing Maps Customizing Biography Content | 43 Figure 3-7. The Shape Tracing developer tools Finding alternate templates To demonstrate the utility of the Shape Tracing module, we’ll create an alternate tem- plate for the content of the “Body” field on our home page. We’re going to add an image to the content and float it to the left of the text. We could use the HTML editor to add an image, but this template will give us a little more control over layout. With the Shape Tracing module activated (click the little square), mouse-over the Content zone. Click when one of the paragraphs is highlighted. The shape tracing con- sole will display information under the “Shape” panel (Figure 3-7). If you expand the “Alternate” tree under the “Shape” panel, you’ll see three possible options for creating an alternate template. Next to each option is a “Create” button that you can click to have the tool generate an alternate template for you in your current theme. Click “Create” to generate the Parts.Common.Body-11.cshtml. Recall that the number in the filename is the unique content ID of a content item and yours will vary. In order to navigate your site normally again, you must click the dash icon in the upper-right corner of the Shape Tracing tool, to turn off its functionality. 44 | Chapter 3: Displaying Content If you use the Shape Tracing tool to create new alternate templates, these files won’t be added to the project automatically. You’ll need to click “Show All Files” in Visual Studio’s Solution Explorer to see them. You can then select “Include in Project” from the context menu. After you click to create this new alternate template, back in the Views directory of the “TheThemeMachine” theme, you’ll find the new file. By default its code just renders the HTML property of the Model property of the page. The Shape Tracing module creates alternate templates based on current template for that piece of content. Now we’ll add the new image for our home page. Create a new media folder by clicking Media→Add a Folder. Name the new folder “Art” and click “Save.” Click into “Art” and then click “Add Media” and upload an image. After it’s been uploaded, click the filename to get its path (copy the “Embed” value). Then back in Visual Studio, open Parts.Common.Body-11.cshtml and add an HTML img tag with the source set to this new image. We’ll render the saved HTML (which is simply the “Body” content) next to our Daisy’s Gone logo: @Model.Html Figure 3-8. The Shape Tracing tool after clicking on a shape Customizing Biography Content | 45 We also could have named the template Parts.Common.Body-Page.cshtml. This naming would have resulted in all pages with a Body part being affected. For our site, that would have meant the home, contact, gallery, and about pages would all include the new image. Even the “News and Notes” HTML widget would be affected. Figure 3-9. The alternate Body template Finding shape properties The Shape Tracing module also provides us with a way to figure out object hierarchies for our template alternates. If you wondered how the expression Model.ContentI tem.AutoroutePart.DisplayAlias was discovered for use in our bio templates, it was the Shape Tracing module. Hover again over a piece of content while the Shape Tracing module is enabled, but this time click the “Model” tab in the console. You’ll see a tree (Figure 3-10) that con- tains information about the ContentItem property of the view’s Model property. You’ll see details of content parts and fields. As you click through the tree, the expres- sion to access these properties appears just below the tabs. Simply copy that expression 46 | Chapter 3: Displaying Content into your alternate template. For example, you could highlight an event in our “Up- coming Events” widget and click through the model to find the location expression @Model.ContentItem.Event.Location. The Shape Tracing module can also be used to see the placement rules for a piece of content, the current template used to display that content and the actual HTML ren- dered for that content. It’s an incredibly powerful tool to have at your disposal. You’ll use it frequently while customizing the display of content on your site. Just don’t forget to disable the module if you’ve enabled it on your live site for testing. Figure 3-10. The Shape Tracing model panel Summary We’ve seen in this chapter that Orchard provides extensive ways to customize content. We can provide alternate renderings for full content items or individual fields within our content. Our alternates were somewhat utilitarian in that they offered improved and new functionality or changed the way something rendered. The second part of customizing content in Orchard is to create striking visual changes to the way content is rendered. We’ll learn how to make such changes in Chapter 4. Summary | 47 CHAPTER 4 Creating Themes In the previous chapter, we saw that it’s possible to customize the look and feel of an Orchard site by creating alternate templates for pieces of content. While this feature does provide some flexibility over how content is rendered on pages, it doesn’t easily allow for wholesale changes to the way a site looks. In order to achieve this broader goal, we’ll look at creating our own themes. We’ve already taken a brief tour of the default theme—“TheThemeMachine”—that is part of a standard Orchard installation. In this chapter, we’re going to take a look inside of that theme to understand how to create our own. At this point, our Orchard development has been limited to editing a couple of view files. We wrote some C# code in those Razor templates and learned a little about how the content is modeled and displayed in a view. However, we haven’t really been ex- posed to the Orchard development experience. To gain this exposure, we first need to learn about a couple of tools that make Orchard development easier. The Orchard Command-Line Interface It probably seems strange to consider using a command-line interface (CLI) with a web- based CMS. However, the Orchard CLI offers quick access to many common admin functions without the need to open up a browser and navigate to different property pages. Assuming you’ve been working in the Orchard solution, the CLI has been ready for use since you first compiled your app and set up your recipe. Getting Help To get started with Orchard’s CLI, open up a command window (or PowerShell) and navigate to the bin directory of your Orchard site. There you’ll find file Orchard.exe. Execute that file. After a few moments, you’ll see an Orchard prompt. PS C:\dev\Orchard> cd .\src\Orchard.Web\bin PS C:\dev\Orchard\src\Orchard.Web\bin> .\Orchard.exe 49 Intializing Orchard Session. (This might take a few seconds...) Type “?” for help, “exit” to exit, cls to clear screen orchard> There are a number of commands you could execute. To get CLI help, there are two commands you should know. The first simply tells you how to perform tasks like quit- ting, clearing the screen, and getting more help: orchard> help One of the items listed when you execute the help command explains how to get help for the commands that allow you to work with your Orchard site: orchard> help commands Executing help commands gives you a list of CLI commands ranging from user creation to page creation. You’ll also see how to go one level deeper for command help: orchard> help page create To test your session, enter the command that simply lists the site cultures, which are used for the internationalization of your site: orchard> cultures list Listing Cultures: en-US Code Generation Tools If you ran the help commands, you probably saw that the Orchard CLI has commands for many of the common tasks you’d normally perform in the admin pages, including enabling and disabling features. We’re going to use this command to enable a feature that will give us additional command line tools: orchard> feature enable Orchard.CodeGeneration Enabling features Orchard.CodeGeneration Code Generation was enabled Once the code generation tools are enabled, they will provide useful shortcuts for cre- ating and managing themes and modules. Orchard.CodeGeneration is an example of the Orchard’s extensibility. In fact, you can build your own command-line tools for Orchard. It’s as simple as creating a class that extends the base class DefaultOrchard CommandHandler. The code generation module is not installed by default with Orchard, but it was found in the source when you downloaded the zip or cloned the repository. If you are not working with the solution, you might need to install this module from the Orchard Gallery. 50 | Chapter 4: Creating Themes Code Generating a Theme In the previous chapter, we saw that a theme is a collection of files contained in a directory in the Themes project in the Orchard solution. There’s nothing special about a theme other than it follows a set of conventions and is stored in the Themes directory under the Orchard.Web project. To start building a new theme, you could simply copy the directory structure for the “TheThemeMachine” theme and paste it into a new sibling directory. We’ll instead use the command line code generation options to create our theme. Later we’ll learn how these tools can help us avoid starting from scratch. The Orchard solution is organized using solution folders, so it appears that the Themes project lives outside of the web project’s directory struc- ture. However, the Themes project and theme files are actually nested in the filesystem under the Orchard.Web directory. Return to the CLI and your Orchard prompt. We’re going to create a new theme named “DaisysTheme.” There are three options we’ll want to consider before we create our new theme: CreateProject Whether to create new project for this theme. The default is false. IncludeInSolution Include this theme in the solution. The default is true. BasedOn Inherit default templates from an existing theme. For our new theme, we’re neither going to inherit from an existing theme nor create a new project. We’ll simply run the theme code generation without any arguments: orchard> codegen theme DaisysTheme Creating Theme DaisysTheme Theme DaisysTheme created successfully Return to Visual Studio where you’ll be prompted to reload the solution. The codegen utility modified the Themes project and forced a reload of the solution. After reloading, we can start to inspect the anatomy of our new theme. Code Generation Tools | 51 The Structure of a Theme The structure might look somewhat familiar to an experienced ASP.NET MVC devel- oper. As is the case with the standard Visual Studio ASP.NET MVC project template, there are directories for Scripts, Styles, and Views. The purpose of each of these and the other directories is as follows: Scripts Directory for JavaScript files Styles Directory for CSS files Views Directory for Razor template (*.cshtml) files Content Directory for images and other static content Zones Directory for templates that wrap zones Additionally, the generated theme template includes a Placement.info file. Recall that this is the XML file that instructs Orchard as to how or whether to layout fields, parts, and items. There are also numerous Web.config files that are used by ASP.NET to set up some configuration plumbing for ASP.NET and ASP.NET MVC. There are two other files worth noting, namely Theme.txt and Theme.png. Both of these files are used by Orchard to describe your theme to the admin pages. Back in the admin dashboard, select Themes→Installed. You’ll see three themes listed (Figure 4-1). The current theme will be the default “TheThemeMachine.” There’s a second theme called “The Journalist” and our new theme named “DaisysTheme.” However, some things don’t look quite right with our new theme. Notice that the name is “DaisysTheme” without a space. Authorship is attributed to “The Orchard Team.” The version is already set to 1.0 and the description and URL are also wrong. As we’ll see shortly, the preview image also fails to accurately represent our new theme at this stage. You might have guessed that Orchard is using Theme.txt and Theme.png to determine what values to plug into this admin page. Before returning to Visual Studio to look at these files, let’s first make our new template the current template by clicking “Set Current.” Once the switch is complete, refresh your site. What you’ll see is that your site is suddenly without any discernible structure or styling (Figure 4-2). Also notice that our alternates are gone. We’ll add some of those pieces before we modify the metadata that’s used by the Dashboard. 52 | Chapter 4: Creating Themes The purpose of this chapter is to introduce theme development. As I am not a designer, I am intentionally keeping the theme we will build sim- ple, including all graphical treatments, styles, and HTML. Default Content Templates If you view the source for the home page, you’ll notice that there is a full HTML docu- ment wrapping the content. This might seem strange, since we haven’t actually defined any master page or layout files. These default template files do exist, though. Orchard includes them in the Orchard.Core project. Figure 4-1. Installed themes The Structure of a Theme | 53 If you expand that project in Visual Studio’s Solution Explorer, you’ll see a Views directory nested under a Shapes directory. The view files inside this directory are used by Orchard as safe defaults for displaying content when no suitable template alternates are found in a theme for a given piece of content. Open the files Document.cshtml and Layout.cshtml to see the HTML that is wrapping the content in our home page. Document.cshtml defines the basic structure of an HTML document, including the html, head, title, and body tags. Layout.cshtml defines a very basic arrangement of div elements on the page. Notice that the template for the title of the page is actually found in the Parts.Title.cshtml file in the Views directory under Title in the Orchard.Core project. As is hopefully now clear, Orchard uses a hierarchy of templates when determining how to render content. In the previous chapter, when we defined alternate templates, we simply added files to the current theme and those files took precedence over those found in the default templates directories. When creating a theme or module (in the next chapter), you could choose to inherit a template or override it at any level (item, type, part, or field). Working with Views In Chapter 3, we created alternate templates in the form of Razor template files. How- ever, we didn’t explore these templates in any detail. Before we build a theme and continue our Razor efforts, it’s worth a quick look at some of the functionality that Orchard adds to the standard base class used by Razor views. For more on Razor, see Programming Razor by Jess Chadwick (O’Reilly). Figure 4-2. A new theme without any styles or structure 54 | Chapter 4: Creating Themes Orchard extends the System.Web.Mvc.WebViewPage base class used by Razor views with its own WebViewPage. We’ve already seen a method from this subclass. When we created our zones, we used its Display method. There are other useful helper methods in this base class. The Style property of WebViewPage has an Include method that will render a link tag that points to the filename provided as its argument. It assumes that your file is in the Styles directory of your theme. Similarly, there is a Script property with methods related to including JavaScript blocks and files. Both the Script and Style properties are of type ResourceRegister, which provides an additional method named Require. Given a resource defined in a manifest class (we’ll learn more about this class in Creating Widgets), Require will find a script or style and ensure that it’s included only once. WebViewPage also includes some convenience methods, such as the null and whitespace checking HasText method. Orchard also provides a StringExtensions class in the Orchard.Utilities.Extensions namespace. This class has methods such as Camel Friendly, Ellipsize, and HtmlClassify, all of which may be useful in views. Layouts For our theme, we’ll consider this document wrapper sufficient and won’t override it with a new Document.cshtml. We’ll instead start our theme with a new layout file. Add a new Razor file named Layout.cshtml to the Views folder in the “DaisysTheme” theme. If you save this file with no content (an empty HTML file) and refresh any page on your site, you’ll see that the content has disappeared. By including a Layout.cshtml file in our template, we’ve instructed Orchard not to use its default layout template. Instead we’ve instructed Orchard to use this new, empty file. Notice though that the page title still appears. As mentioned, the title was included in Document.cshtml, which we chose not to override. If you’ve viewed the source of any of your site’s pages as we’ve been making changes to the theme, you may have noticed a great deal of JavaScript content. The script is from the Shape Tracing module that we enabled in the previous chapter. It would not appear on production sites unless you left that module enabled. We’ll add some code to Layout.cshtml to get some content back on our site. Start by adding the following snippet:
Layouts | 55 This simple chunk of Razor code demonstrates a couple of key patterns for building layouts in Orchard. We’re going to explore this pattern in more detail later in this chapter. For now, recognize that we null-check a property on our view’s Model and call Display on that property when it’s not null. Recall that the data bound to our views are dynamic types known as shapes. The Model property of our view is a representation of these shapes. When you call the Display method, it’s going to check the runtime type of the argument you provided. In this case, the type will contain metadata indicating that it’s a “zone,” which will allow the Display method to properly render content in that given zone. The Display method is actually a read-only dynamic property that is de- fined in Orchard’s WebViewPage, which is the base page type for Razor views in Orchard. This property returns an instance of a callable dynamic object, which is why the method-call syntax works. Saving the layout and refreshing the home page, we now see that the main content has been added. However, we’ve lost our navigation and other zones. Let’s add the navi- gation by placing the block of code that follows above the content snippet we previously entered: @if (Model.Navigation != null) {
@Display(Model.Navigation)
} Once navigation has been added, we can now refresh the home page and click through each of the pages to see that content is in fact displaying on each page. While most pages look like their “TheThemeMachine” equivalents without any CSS, the home page is noticeably missing the widgets we’d previously added. If we want the Bing Maps widget to show up in our new theme, we need to include a zone named TripelThird and a block of Razor/HTML as follows: @if (Model.TripelThird != null) {
@Display(Model.TripelThird)
} Zones and Layers At this point we can see that zones are simply sections defined in our layout templates. We use the Display method of the view to create them. Adding widgets to zones in the admin tool creates the relationship that allows the null-checks that surround the Dis play calls to evaluate properly. Had we not added a widget to the zone TripelThird, then Model.TripelThird would evaluate to null. 56 | Chapter 4: Creating Themes Let’s take a quick detour from Daisy’s Theme to explore zones a little deeper. Start by opening up Theme.txt in our theme’s root directory. The “Zones” entry in this file is used by Orchard to display the list of zones that appear when you click “Widgets” on the admin menu. That list is currently populated by zones defined in other installed themes and Orchard will tell you as much when you visit that page. Add a “Zones” section to your Theme.txt named “MoreContent”: Zones: MoreContent Next return to Layout.cshtml and add a new zone: @if (Model.MoreContent != null) {
@Display(Model.MoreContent)
} If you refresh the “Widgets” admin page, you’ll now see a zone named “MoreContent” above the zones defined in “TheThemeMachine.” Click Add→Html Widget, add some content, and save. Next, refresh the home page (or any other page). You’ll now see that the zone is displaying on each page (Figure 4-5). Let’s limit this new widget so that it appears only on the home page. Click on the “More Daisy’s Content” link (the name of the HTML widget) listed with the “MoreContent” zone. On the property page for that widget, choose the layer named “TheHomepage” and click Save. Click through to each of the pages to see that the layer rule has enabled this zone only for the home page. Alternate Templates As you navigate around the site, you’ll notice that we’ve lost the customization built in Chapter 3 for rendering bios and events. If we want to get these templates back, all we need to do is add those Razor files into our new template. If you move or copy Content-Bio.Summary.cshtml to the Views directory of our “Dai- sysTheme” theme and refresh the bio page, you’ll see the listed bios are displaying content as they were previously. Of course, without any CSS in our theme you’ll notice that the rendering lacks any style (Figure 4-4). Theme Inheritance At this point, our theme isn’t particularly stylish or interesting. Our layout is pretty limiting as well. What we really want is some HTML that’s easily styled by a skilled designer. Fortunately, the work for that has already been done. The theme “TheThemeMachine” defines a very flexible layout file. We could simply copy that into our theme, but instead we’re going to inherit it into our theme. In Alternate Templates | 57 “DaisysTheme” open Theme.txt and add the line that follows. Then delete Lay- out.cshtml (the one we created): BaseTheme: TheThemeMachine After you refresh the home page, you’ll see that our site has returned to its “TheThe- meMachine” roots. However, we obviously want to customize our look and feel a bit. To deviate from the inherited theme, we need to override the default styles found in “TheThemeMachine.” Create a new file Site.css in the Styles directory of the “DaisysTheme” theme. After you create the empty stylesheet, you’ll see after refreshing your site that we’ve again lost our styling, but maintained our layout and alternate templates (Figure 4-5). Figure 4-3. The MoreContent zone 58 | Chapter 4: Creating Themes Unfortunately, there’s no way for our theme to inherit both the layout and stylesheet from the “TheThemeMachine” theme. Layout.cshtml explicitly includes only a single stylesheet named Site.css. If we want to inherit the entire layout file and customize the style, we have to copy the contents of Site.css from the Styles directory of “TheTheme- Machine” into our new file. Otherwise, we have to modify the layout file to look for an additional stylesheet. Figure 4-4. An alternate template in the DaisysTheme theme Alternate Templates | 59 Basic Styling Copy the stylesheet content over into our new theme (Site.css) and save the stylesheet file. Refresh the site to see that we’re now back to the “TheThemeMachine.” Again, we’ll leave the design lessons for the designers, but we’ll modify some of the basic UI to make our theme a little more unique. Since we’re designing a site for a rock band, we’ll change the background color to a blackish color. Locate the body selector in the stylesheet and we’ll go from a light theme to dark simply by changing the background color: body { line-height: 1; font-size: 81.3%; color: #434343; background: #303030; font-family: Tahoma, "Helvetica Neue", Arial, Helvetica, sans-serif; } Figure 4-5. DaisysTheme inheriting layout from TheThemeMachine 60 | Chapter 4: Creating Themes Of course, it’s a bit hard to read the gray text on the dark-gray background. So let’s lighten up our content areas. We could go into the individual page sections and set each to have a white background, but there’s an easier way. We can take advantage of the fact that the layout from which we’re inheriting wraps various groups of zones in div elements with a class name of “group”: .group { background: #fff; } Let’s also update the font that’s used for the header of the site. By default it uses a font named “Lobster.” You’re probably thinking that you don’t have “Lobster” installed, yet you’ve somehow been seeing the correct cursive font on the header. Orchard as- sumes modern web standards by default, so our theme is able to make use of the @font- face directive in CSS3. More specifically, it uses the Google Web Fonts API: Style.Include("http://fonts.googleapis.com/css?family=Lobster"); If we want to change this font, we have a few options. We’re again faced with the dilemma of whether to modify or copy Layout.cshtml in “TheThemeMachine” to in- clude our desired change. Since we’re just changing styles, we’re going to keep the layout in place and take a different approach. We’ll simply import a new web font in our stylesheet and then set the branding ele- ment’s font-family to our new font. Start by adding a new @import directive to the top of our stylesheet: @import url(http://fonts.googleapis.com/css?family=Frijole); In our template, the header text is rendered in an h1 element named “branding.” We’ll simply set style for that element to use our newly imported font: #branding a { text-decoration:none; color: #434343; font-family: 'Frijole'; } Styling Projections Next we’re going to modify the event listing so that the event titles have a background color and text that’s in all caps. We could write our CSS selector expression to affect all a tags that follow h1 tags as that’s the way events are rendered, but such a selector would not be limited to events matching that pattern. Instead we’re going to inject a class name into each event row. In the admin dashboard, navigate to “Queries” and select the row for “All Events.” Click to edit the “1 columns grid” that we created previously. In the section with the heading “Html properties,” enter a value “event-row” under the “Row class” and save. Alternate Templates | 61 We could also have chosen from predefined dynamic expressions (in the drop-down menu for that field), but a static class is sufficient for our purposes. After you save the new row class, add a new CSS rule to affect a elements that follow h1 elements that follow a tr element with a class name of “event-row.” We’ll style the anchors to be displayed as “block” so that we have equal length backgrounds: tr.event-row h1 a { background-color:#BACEFF; padding:3px; color:#000; width:300px; display:block; } Figure 4-6 shows the template with our new styling. Shape Wrapping We’re going to add another template to our theme, but first we have to bring back the rest of our content customization. Copy or move Content-Bio.cshtml, Parts.Common.Body-11.cshtml, and Placement.info from “TheThemeMachine” to “DaisysTheme.” Figure 4-6. A styled projection 62 | Chapter 4: Creating Themes After moving those files, add a new template named NewsAndNotes.Wrapper.cshtml. Unlike our other templates, this one will surround its target with HTML and won’t actually modify the shape template itself. To call attention to band activity, the code for this wrapper will simply add a div element with a yellowish background to our “News and Notes” HTML widget:
} After importing the Orchard.Utilities.Extensions namespace, we define three vari- ables to be used in the rendering of our field. Next, the display option chosen by the creator of the content type is interrogated to determine how we should display the place on our content item (e.g., an Event item). The logic here is fairly straightforward. The CamelFriendly extension method required declaring the string name variable since we can’t use extension methods with dynamic ex- pressions such as Model.Name. Next we’ll create the editor templates. In the Views directory, create a new directory named EditorTemplates with a subdirectory named Fields. Add to that directory a file named Contrib.Places.cshtml. This template will provide a simple UI for selecting a category, inputting a postal code and then selecting the place. We’ll skip validation for the sake of keeping the form from getting more complicated: @using Orchard.Utility.Extensions @model Contrib.PlacesField.ViewModels.PlacesFieldViewModel @{ Style.Require("jQueryUI_Orchard"); Script.Require("jQuery"); Script.Require("jQueryUtils"); Script.Require("jQueryUI_Core"); Script.Require("jQueryUI_Widget"); Script.Require("jQueryUI_Autocomplete"); } @using (Script.Foot()) { } It might look like there’s a lot happening in this view, but it’s relatively straightforward. We’re using the jQuery UI Autocomplete plugin to render our short list of Yelp cate- gories. The autocomplete code for categories and places is in the script block at the bottom of our Razor template. How the Autocomplete plugin works isn’t important for understanding how to create a field, so we’ll skip a detailed explanation. The important thing to understand is that we’ve created a form with three fields for collecting information relevant to our places selection. We use hidden fields to save category and places data that is persisted, but not displayed to the user (category ID, latitude, and longitude). For Orchard to know where to put our field, we’ll need to create a Placement.info file at the root of our module project. The entries in this file will allow our templates to appear in both admin forms and content displays. We saw how placement files work in Displaying Content. Without this file, your field will not display: Settings Next we’ll take care of persisting the settings. We’re going to need to hook into admin editor events in order to save our settings. This code is mostly boilerplate and you’ll find yourself copying, pasting, and modifying it as you create your own fields that require settings. Start by creating a new file named PlacesFieldEditorEvents.cs in the Settings directory that we created earlier. In PartFieldEditor, we help Orchard load our settings form within the context of a content type editor form (we’ll see where this form is rendered before the end of the chapter). PartFieldEditorUpdate is used to handle the actual saving of our settings. The Places Field Project | 75 These two methods are similar to Editor and EditorTemplate in our field’s driver class: using System.Collections.Generic; using Contrib.PlacesField.Settings; using Orchard.ContentManagement; using Orchard.ContentManagement.MetaData; using Orchard.ContentManagement.MetaData.Builders; using Orchard.ContentManagement.MetaData.Models; using Orchard.ContentManagement.ViewModels; public class PlacesFieldEditorEvents : ContentDefinitionEditorEventsBase { public override IEnumerable PartFieldEditor(ContentPartFieldDefinition definition) { if (definition.FieldDefinition.Name == "PlacesField") { var model = definition.Settings.GetModel(); yield return DefinitionTemplate(model); } } public override IEnumerable PartFieldEditorUpdate( ContentPartFieldDefinitionBuilder builder, IUpdateModel updateModel) { var model = new PlacesFieldSettings(); if (builder.FieldType != "PlacesField") { yield break; } if (updateModel.TryUpdateModel( model, "PlacesFieldSettings", null, null)) { builder.WithSetting("PlacesFieldSettings.DisplayOptions", model.DisplayOptions.ToString()); } yield return DefinitionTemplate(model); } } To create the editor template for our settings, create a new file named PlacesFieldSet- tings.cshtml in a new directory under Views named DefinitionTemplates. Note that this template will appear when creating content types, not content items. It’s the creator of the type, not the content item that will set this value. In our template, we’ll simply render an HTML select list from the values in our PlacesFieldDisplayOptions enumeration: @model Contrib.PlacesField.Settings.PlacesFieldSettings @using Contrib.PlacesField.Settings; Controllers One final important detail is the data. So far, we’ve created a pretty complex field, but it won’t actually do anything until we provide it a way to get data from the Yelp API. As I previously mentioned, the API is a simple REST API. However, we can’t query it directly from our views, because browser security restrictions won’t allow AJAX re- quests to other servers. To solve this problem, we’ll have to create a solution that runs on our Orchard site. Specifically, we’re going to create an ASP.NET MVC controller class that will handle the AJAX requests that are generated by the jQuery Autocomplete plugin. If you revisit the code for our editor view, you’ll see the URLs that we’re going to call. URL to retrieve Yelp Categories /Admin/PlacesField/Yelp/Categories URL to retrieve Yelp Places /Admin/PlacesField/Yelp/Places If you’re familiar with MVC, you probably recognize that we’re going to create a con- troller named YelpController with two action methods, Categories and Places. We’re going to examine the controller shortly, but for now we need to setup a route to tell Orchard and MVC how to map our requests. Orchard will automatically map all routes by finding classes that implement IRouteProvider. Save this file as Routes.cs at the root of the module project: using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; using Orchard.Mvc.Routes; public class Routes : IRouteProvider { public void GetRoutes(ICollection routes) { foreach (var routeDescriptor in GetRoutes()) routes.Add(routeDescriptor); } public IEnumerable GetRoutes() { The Places Field Project | 77 return new[] { new RouteDescriptor { Priority = 5, Route = new Route( "Admin/PlacesField/{controller}/{action}", new RouteValueDictionary { {"area", "Contrib.PlacesField"}, {"controller", "Yelp"}, {"action", "Places"} }, new RouteValueDictionary(), new RouteValueDictionary { {"area", "Contrib.PlacesField"} }, new MvcRouteHandler()) } }; } } The area defined in the RouteValueDictionary must be the name of the module and not an area that is defined as a URL part as is expected with typical MVC routing. The Controller class is a standard MVC controller. Save this file as YelpController.cs in the Controllers directory that the code generation tools created for the module project: using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using YelpSharp; using YelpSharp.Data.Options; public class YelpController : Controller { private readonly Yelp _client; public YelpController() { var options = new Options() { AccessToken = "", AccessTokenSecret = "", ConsumerKey = "", ConsumerSecret = "" }; _client = new Yelp(options); } public YelpController(Yelp client) { _client = client; } 78 | Chapter 5: Creating Modules public ActionResult Places(string postalCode, string category, string term) { var so = new SearchOptions() { LocationOptions = new LocationOptions() { location = postalCode }, GeneralOptions = new GeneralOptions(){ category_filter = category, term = term } }; var results = _client.Search(so); return Json(results.businesses.Select( p => new { value = p.location.coordinate.latitude + "~" + p. location.coordinate.longitude, label = p.name }).ToArray() , JsonRequestBehavior.AllowGet); } public ActionResult Categories(string postalCode, string categories, string term) { var categoryList = new List { "Restaurants", "Nightlife", "Arts" }; var results = string.IsNullOrEmpty(term) ? categoryList : categoryList .Where(c => c.ToLower() .Contains(term.ToLower())); return Json(results.Select( c => new { value = c.ToLower(), label = c }) .ToArray(), JsonRequestBehavior.AllowGet); } } Most of the logic in the controller deals with retrieving data from the Yelp service. For this code to work, you’ll need to add the YelpSharp Nuget package to your field project: PM> Install-Package YelpSharp You’ll need to get free API credentials from http://www.yelp.com/devel opers/getting_started. These API credentials then need to be entered into the constructor of the controller, where the Yelp client is instantiated. If you forget this step, your field won’t work. Request the 2.0 API on the API access page. It will give you all four keys that you’ll require. The Places Field Project | 79 Again, this class is a standard MVC controller. There isn’t anything Orchard-specific to consider here. Basically, all that’s happening in each action (public method that returns an ActionResult) is that we’re querying the Yelp API method and returning a JSON serialized result of the Yelp data. The YelpSharp client takes care of deserializing the Yelp response to POCO classes that we’ll then serialize to JSON formats that our autocomplete textboxes will consume. We use a LINQ projection to create a JSON structure that is friendly to the Autocom plete plugin. Module Metadata The final step is to set the metadata for our field in Module.txt. These settings are straightforward and include author and module description. If we had multiple fields in our project we could list multiple features, which could be enabled or disabled sep- arately. Finally, we’ll also declare a set of modules upon which our field is dependent: Name: Places Field AntiForgery: enabled Author: John Zablocki Website: http://dllhell.net Version: 1.0 OrchardVersion: 1.3 Description: The Places Field allows for location lookups Features: Contrib.PlacesField: Name: Places Field Description: Places fields. Category: Fields Dependencies: Orchard.jQuery, Common, Settings Using the Places Field Now that we’ve created our field, it’s time to compile the project and enable the module. Build your project to make sure you’ve successfully added all the using directives and Nuget references that our code demands. Then return to your Orchard command-line session and enable the new module: PM> feature enable Contrib.PlacesField Enabling features Contrib.PlacesField Places Field was enabled Creating Content with the Places Field Now that our field is coded and enabled, it’s time to test it out. Return to the Dashboard and select Content→Content Types and select “Edit” on the listing for “Event.” Click “Remove” on the field listing for the existing “Location” field. Then click “Add Field” and select the field type “Places Field.” Name the field “Event Location,” accepting the 80 | Chapter 5: Creating Modules default technical name as usual. Click “Save” and the “Event Location” field will appear in the list of fields. We could add several PlacesField elements to our Event content type if we wanted to, which is why we had to prefix our autocompleter client side IDs with field specific IDs. In other words, when you select a place for PlacesFieldA it won’t get mixed up with PlacesFieldB. Click “>” next to the field name to be presented with our settings editor. It’s here while defining our content type that we’ll set the display option for this field, affecting all items created from this content type definition. From the select list with our display options (Figure 5-1), choose “Name and Embedded Map” and click Save. Figure 5-1. The Places Field settings template If you were to browse to the “Events” listing page on our site, you’d see a Bing Maps error because we haven’t selected a place and we set our field to render a map. You should also see a Bing Maps error on your content item listing (Content→Content Items), because Orchard uses the same field display template to show a preview of your content item in the admin content listing page. That template includes the map code. Using the Places Field | 81 To fix these errors, we’ll simply select a place. Click Content→Content Items and then click “Edit” on the event listing you wish to edit. As you start typing in the “Category” field, you should see a list of categories that match your input. After selecting a category and entering a zip code, go ahead and search for a place by typing in the “Place” textbox. Yelp provides a full list of available categories that can be used to filter out results at http://www.yelp.com/developers/documentation/category _list. This module limits searches to only those categories that seem likely to host live music. A more robust solution would also provide some better caching and filtering of results. Figure 5-2. The Places Field editor template After you’ve selected a place, click “Publish Now” and then return to the site and refresh the “Events” page. You should now see the event location displayed with both the name of the place you selected and an embedded Bing map (Figure 5-3). 82 | Chapter 5: Creating Modules Figure 5-3. The Places Field displayed with a Bing map Displaying the Places Field If you click over to the home page (or any page that isn’t the “Events” page) you’ll see that our event location map is overtaking the space it’s been allotted in the AsideSec ond zone. Our “Upcoming Events” HTML widget is trying to use the embedded map in a smaller space than our event listing or event detail pages. In Chapter 3, we learned how to customize the display of fields and other types of content. Our own fields may be customized in the same way. Copy Contrib.Places.cshtml from the Fields directory under the Views directory in our mod- ule. Paste it in the Views directory of our “DaisysTheme” theme and rename it Fields.Contrib.Places.cshtml. Change the height and width of the iframe that includes the map and refresh the “Events” page. You’ll see that the embedded map has changed to reflect your chosen size: