We’ve already seen that Orchard may be extended easily and quickly by
installing modules from the Gallery. When we wanted to add an image field to
our event pages, we just installed the ImageField
module. When we didn’t want to create
our own contact form, we just installed one that someone had already written
and shared. So what happens when we want functionality that isn’t readily
available in the Gallery? Well, we just have to trade instant gratification
for a little elbow grease.
Some of the most interesting data sources available to developers are those that contain places information. These are the databases that allow you to check into a business or some other venue with Facebook or Foursquare. When you arrive somewhere, your phone (or HTML5 capable browser) sends your location to a server-side API where a list of businesses, restaurants, and other public points of interest is then returned to the phone. You select the one where you are and check in.
While it’s unlikely that users will be checking into the Daisy’s Gone site, there’s another use of places data of which we might wish to take advantage. Consider the case of Meetup.com, where users who want to schedule a Meetup for their group are asked to select a location before saving the new event. The location is chosen from an auto-suggested list of places, similar to the check-in features of Foursquare and Facebook.
This is the functionality we’re going to add to our Event
content type. Currently, our Location
field is a text field into which event creators enter free-form text. In
order to limit the locations to valid venues, we’ll instead use a field
that auto-suggests possible places given a center point. To get this
functionality, we’ll need a module. Fortunately for us, as of this
writing, there is no such module in the Orchard Gallery. I say
“fortunately” because we’re going to have the opportunity to create it in
this chapter.
There are several places where we could get our places data. Facebook and Foursquare provide places data that is somewhat coupled with their respective services. Google provides places data, but doesn’t allow that data to be rendered on a Bing Map (or any other non-Google Maps service). SimpleGeo provides places data, but it requires a paid developer key. For our needs, Yelp provides an API that will suit us well.
Yelp offers a REST API that is easily consumed from a .NET
application. For our project, we’ll use the YelpSharp
library that’s available on Nuget.
We’re not really concerned with the inner workings of a RESTful API
client, so we’ll just install the client and use it as a black
box.
When we created our new theme in Chapter 4, we used the Orchard.CodeGeneration module to create the initial directory structure for our theme’s files. The new files were added to the Themes project that is part of the main Orchard solution. Unlike themes, each module is housed in its own project by default. Within the solution, these projects are organized under the “Modules” solution folder. On disk, these projects are stored in the Modules directory beneath the Orchard.Web directory.
Running the code generation command to create a new module will result in a new C# project being added to the solution within the Modules solution folder. If you’re familiar with ASP.NET MVC, you’ll probably recognize the structure, with familiar directories such as Views and Controllers. As we build out our module we’ll explore the project in more detail.
Unlike the other modules that typically ship with Orchard, the code generated module projects are not web projects, but rather standard class library projects. What this difference means is that some of the context menus will not be web-centric (i.e., there is no “Add Controller”).
As we did when creating our theme, we’ll use PowerShell (or the standard command line) to create an Orchard session. If you closed out your session from the previous chapter, navigate to the Orchard.Web project’s bin directory. Once there, execute Orchard.exe to initiate the session:
orchard> codegen module Contrib.PlacesField
Creating Module Contrib.PlacesField
Module Contrib.PlacesField created successfully
The default behavior of the codegen
utility is to add the new module project
to the solution. So when you return to your solution in Visual Studio,
you’ll be prompted to reload. Once you do, your new module project will be
available. The module project is mostly comprised of empty directories and
a few web.config files at this point.
There’s also a Module.txt file that
will describe our module to the admin Dashboard. Again, we’ll explore
these project files as we start building out our new module.
We’re going to create a content field, which is a little different than creating a more complex content part. Each has its own conventions. We’ll examine content part creation in the next chapter on our way to creating a widget. However, some of the steps and concepts we’ll use while creating our field will certainly provide us with transferable knowledge when it comes time to create a more complex module.
Before we add any code to the project, let’s review the goal of our field to make sure we understand its requirements. At a high level, we simply want to limit the list of locations for an event to those that come from the Yelp API. To do so, we’ll ask admin users (or content creators) to provide some details that will narrow the places search. After providing those pieces of information, users may then select a place from the list. That place and its details will then be saved and made available for rendering on the event pages (or any other content items where this field is used).
The Yelp API is highly flexible, allowing for searches by category, distance, search terms, and more. For the sake of keeping things simple our field won’t support all possible query options, though it should be sufficiently generic to allow for reuse.
We’ll start off by considering the model for our field. Our model
will represent the query and its result. To that end, we’ll create a
model class that has properties for some of the possible search
parameters that we’ll use when querying the API. This model class will
be a subclass of Orchard’s ContentField
, which will provide our class
with a few basic services that we’ll see as we build our field.
Under the new Contrib.PlacesField
project, create a new
directory named Fields and add a
class to that directory named PlacesField.cs. This class is just a plain
old CLR object (POCO) that uses the persistence capabilities of the
Storage
property from its base class
to save and retrieve the values set by content creators:
using Orchard.ContentManagement; namespace Contrib.PlacesField.Fields { public class PlacesField : ContentField { public string PostalCode { get { return Storage.Get<string>("PostalCode"); } set { Storage.Set<string>("PostalCode", value); } } public string Category { get { return Storage.Get<string>("Category"); } set { Storage.Set<string>("Category", value); } } public string PlaceName { get { return Storage.Get<string>("PlaceName"); } set { Storage.Set<string>("PlaceName", value); } } public string PlaceLatLong { get { return Storage.Get<string>("PlaceLatLong"); } set { Storage.Set<string>("PlaceLatLong", value); } } } }
In our admin pages, we’re going to collect information that is
specific to a content item that is using our field,
namely a postal code, category, and the selected place. We’re also going
to collect settings from content type creators that
will affect how all content items built from a content type definition
behave. We’ll create the data model for our settings shortly and see how
this all ties together when we update our Event
type.
We’ve just defined a mixed data model that combines type-level settings with item-level data. The settings and data will both be used to render places by changing the behavior of the view appropriately. To bridge the gap between our disparate models and the denormalized view, we’ll create a class that knows about both but more closely resembles the view. This approach is commonly known as the Model-View-ViewModel (MVVM) pattern, and it is frequently used in ASP.NET MVC applications.
Create a new folder named ViewModels and add a file named PlacesFieldViewModel.cs. This class will look very similar to our actual model class, but it won’t concern itself with persistence. It will also have a simple string property for our category IDs and two additional properties for keeping track of our view options (the global settings):
namespace Contrib.PlacesField.ViewModels { public class PlacesFieldViewModel { public string Name { get; set; } public string PostalCode { get; set; } public string Category { get; set; } public string PlaceName { get; set; } public string PlaceLatLong { get; set; } public bool ShowLink { get; set; } public bool ShowMap { get; set; } } }
As hinted at, we’re also going to include an option that will
allow admins to decide which way to render the selected place on content
pages. We’ll provide options for displaying only the name of the place,
displaying the name with a link to Bing Maps, or with an embedded Bing
Map (using an iframe
element, not the
widget).
The way we’ll represent these options in our model will be to
create a settings class. This class is just another POCO and will be
used by the admin pages that manage content types that use our field.
We’ll include a simple enumeration for the options. Create a new folder
named Settings and add a file
PlacesFieldSettings.cs with the
following content (you can include the enum
definition in this file):
namespace Contrib.PlacesField.Settings { public enum PlacesFieldDisplayOptions { NameOnly, NameAndLinkToMap, NameAndEmbeddedMap } public class PlacesFieldSettings { public PlacesFieldDisplayOptions DisplayOptions { get; set; } } }
Now that we’ve created our supporting data classes, it’s time to write some code to take care of the actual rendering and saving of our field data. The class that has these responsibilities is known as a “driver.” We’ll also see drivers when we build a content part in Chapter 6.
If you’re familiar with ASP.NET MVC, you could think of drivers as being analogous to controller classes that are tightly coupled to a module. Drivers will be responsible for rendering our field’s admin (when creating content) and content item (when displaying content) templates.
Create a new directory named Drivers and add a file named PlacesFieldDriver.cs. This new class will
extend ContentFieldDriver
, which will
allow Orchard to recognize that this module is in fact a content field.
There are three methods we’ll override in this class, which are
discussed next. Though it certainly looks complex, the Display
and Editor
methods are simply either passing data
to a view or reading data back from a form post:
using Orchard.ContentManagement.Drivers; using Contrib.PlacesField.Settings; using Orchard.ContentManagement; using Contrib.PlacesField.ViewModels; namespace Contrib.PlacesField.Drivers { public class PlacesFieldDriver : ContentFieldDriver<Fields.PlacesField> { //methods shown below } }
The Display
method begins by
retrieving the saved settings for our field (we’ll write code to save
these shortly). The ContentField
class that we extended to create our PlacesField
provides the functionality to
retrieve these settings. After getting the settings, the ContentShape
method is used to return a shape
to the view that will be used for display.
Note that this code won’t compile until we complete the rest of the dependent code we write later in this chapter, so don’t worry if you see several missing references.
Recall that the content that we bind to our views is rendered as a
special dynamic
type called a shape.
The lambda expression provided as our third argument to ContentShape
is creating the actual shape that
will be used as the model for our PlacesField
view:
protected override DriverResult Display(ContentPart part, Fields.PlacesField field, string displayType, dynamic shapeHelper) { var settings = field.PartFieldDefinition .Settings .GetModel<PlacesFieldSettings>(); return ContentShape("Fields_Contrib_Places", field.Name, s => s.Name(field.Name) .PlaceName(field.PlaceName) .PlaceLatLong(field.PlaceLatLong) .ShowLink(settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndLinkToMap) .ShowMap(settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndEmbeddedMap) ); }
C# 4.0 introduced the dynamic keyword, which when applied to a variable, instructs the compiler not to check for compile-time correctness of properties and methods. In other words, you are letting the compiler know that everything will evaluate correctly at runtime. Orchard makes heavy use of this dynamic feature (e.g., shapes). Without C# 4.0, Orchard could not exist as it’s written today.
The first Editor
method is used
to load our field into the dashboard editor forms for content types
using our field (i.e., Create→New→Event). As was the case with Display
, Editor
also begins by retrieving the saved
settings. It then composes a ViewModel instance from both settings and
persisted field values. ContentShape
is again called, but in this case, the EditorTemplate
method of the dynamic shapeHelper
is used to load our editor
template and provide our ViewModel instance as the view’s Model
property:
protected override DriverResult Editor(ContentPart part, Fields.PlacesField field, dynamic shapeHelper) { var settings = field.PartFieldDefinition .Settings.GetModel<PlacesFieldSettings>(); var viewModel = new PlacesFieldViewModel { Name = field.Name, Category = field.Category, PostalCode = field.PostalCode, ShowLink = settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndLinkToMap, ShowMap = settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndEmbeddedMap, PlaceName = field.PlaceName, PlaceLatLong = field.PlaceLatLong }; return ContentShape("Fields_Contrib_Places_Edit", () => shapeHelper.EditorTemplate( TemplateName: "Fields/Contrib.Places", Model: viewModel, Prefix: getPrefix(field, part) )); }
The second Editor
method
handles the post-back for our field when a user saves a content item
that was defined to use our new field. After saving changes and mapping
posted values to our field instance, the edit template is redisplayed
(by way of the content item’s editor form):
protected override DriverResult Editor(ContentPart part, Fields.PlacesField field, IUpdateModel updater, dynamic shapeHelper) { var viewModel = new PlacesFieldViewModel(); if (updater.TryUpdateModel(viewModel, getPrefix(field, part), null, null)) { var settings = field.PartFieldDefinition .Settings.GetModel<PlacesFieldSettings>(); field.Category = viewModel.Category; field.PostalCode = viewModel.PostalCode; field.PlaceName = viewModel.PlaceName; field.PlaceLatLong = viewModel.PlaceLatLong; viewModel.ShowLink = settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndLinkToMap; viewModel.ShowMap = settings.DisplayOptions == PlacesFieldDisplayOptions.NameAndEmbeddedMap; } return Editor(part, field, shapeHelper); }
The private getPrefix
method is
used to create unique column values in the database for our field. It’s
a convention to define this method, not a requirement:
private static string getPrefix(ContentField field, ContentPart part) { return (part.PartDefinition.Name + "." + field.Name) .Replace(" ", "_"); }
Next we’re going to create some templates for our field. We’ll need both an editor template and a standard (non-admin) view template. We’ll start with the view template, because it is reasonably straightforward. In the Views directory, create a new directory named Fields and add to it a file named Contrib.Places.cshtml:
@using Orchard.Utility.Extensions @{ string name = Model.Name; string mapLink = "http://www.bing.com/maps/?v=2&cp=" + @Model.PlaceLatLong + "&lvl=12&dir=0&sty=r&where1=" + @Model.PlaceLatLong.ToString().Replace("~", ","); string iframeSource = "http://www.bing.com/maps/embed/?v=2&cp=" + @Model.PlaceLatLong + "&lvl=15&dir=0&sty=r&where1=" + @Model.PlaceLatLong.ToString().Replace("~", ",") + "&form=LMLTEW&pp=" + @Model.PlaceLatLong + "&emid=30e5aeb2-f963-4089-e9b3-9bd5dfe07759"; } <p class="text-field"> @name.CamelFriendly(): @if (!Model.ShowLink && ! Model.ShowMap) { <text>@Model.PlaceName</text> } @if (Model.ShowLink) { <a href="@mapLink" target="_blank">@Model.PlaceName</a> } @if (Model.ShowMap) { <text>@Model.PlaceName</text> <div id="mapviewer"> <iframe id="map" scrolling="no" width="400" height="300" frameborder="0" src="@iframeSource"> </iframe> </div> } </p>
After importing the Orchard.Utilities.Extensions
namespace, we
define three variables 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 expressions 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"); } <fieldset> <label for="@Html.FieldIdFor(m => Model.PlaceName)"> @Model.Name.CamelFriendly()</label> <p /> <label class="forpicker" for="@Html.FieldIdFor(m => Model.Category)_CategoriesSelector">Category</label> <input type="text" id="@Html.FieldIdFor(m => Model.Category)_CategoriesSelector" class="text"/> @Html.HiddenFor(m => m.Category) <p /> <label class="forpicker" for="@Html.FieldIdFor(m => Model.PostalCode)">Postal Code</label> @Html.EditorFor(m => m.PostalCode) <p /> <label class="forpicker" for="@Html.FieldIdFor(m => Model.PlaceName)">Place</label> @Html.EditorFor(m => m.PlaceName) @Html.HiddenFor(m => m.PlaceLatLong) </fieldset> @using (Script.Foot()) { <script type="text/javascript"> function split(val) { return val.split(/,s*/); } function extractLast(term) { return split(term).pop(); } $(function () { $("#@Html.FieldIdFor(m => Model.Category)_CategoriesSelector") .autocomplete({ source: '@Url.Content("~/Admin/PlacesField/Yelp/Categories")', minLength: 2, select: function (event, ui) { $("#@Html.FieldIdFor(m => Model.Category)") .val(ui.item.value); $("#@Html.FieldIdFor(m => Model.Category)_CategoriesSelector") .val(ui.item.label); return false; } }); $("#@Html.FieldIdFor(m => Model.PlaceName)").focus(function () { $("#@Html.FieldIdFor(m => Model.PlaceName)").autocomplete({ source: '@Url.Content("~/Admin/PlacesField/Yelp/Places")' + '?categories=' + $("#@Html.FieldIdFor(m => Model.Category)").val() + '&postalCode=' + $("#@Html.FieldIdFor(m => Model.PostalCode)").val(), minLength: 1, select: function (event, ui) { $("#@Html.FieldIdFor(m => Model.PlaceLatLong)") .val(ui.item.value); $("#@Html.FieldIdFor(m => Model.PlaceName)") .val(ui.item.label); return false; } }); }); }); </script> }
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 categories. 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:
<Placement> <Place Fields_Contrib_Places_Edit="Content:2.5"/> <Place Fields_Contrib_Places="Content:2.5"/> </Placement>
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.
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<TemplateViewModel> PartFieldEditor(ContentPartFieldDefinition definition) { if (definition.FieldDefinition.Name == "PlacesField") { var model = definition.Settings.GetModel<PlacesFieldSettings>(); yield return DefinitionTemplate(model); } } public override IEnumerable<TemplateViewModel> 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 PlacesFieldSettings.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; <fieldset> <label for="@Html.FieldIdFor(m => m.DisplayOptions)" class="forcheckbox">Display options</label> <select id="@Html.FieldIdFor(m => m.DisplayOptions)" name="@Html.FieldNameFor(m => m.DisplayOptions)"> @Html.SelectOption(PlacesFieldDisplayOptions.NameOnly, Model.DisplayOptions == PlacesFieldDisplayOptions.NameOnly, "Name only") @Html.SelectOption(PlacesFieldDisplayOptions.NameAndLinkToMap, Model.DisplayOptions == PlacesFieldDisplayOptions.NameAndLinkToMap, "Name and Link to Map") @Html.SelectOption(PlacesFieldDisplayOptions.NameAndEmbeddedMap, Model.DisplayOptions == PlacesFieldDisplayOptions.NameAndEmbeddedMap, "Name and Embedded Map") </select> @Html.ValidationMessageFor(m => m.DisplayOptions) </fieldset>
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 requests 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.
/Admin/PlacesField/Yelp/Categories
/Admin/PlacesField/Yelp/Places
If you’re familiar with MVC, you probably recognize that we’re
going to create a controller 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<RouteDescriptor> routes) { foreach (var routeDescriptor in GetRoutes()) routes.Add(routeDescriptor); } public IEnumerable<RouteDescriptor> GetRoutes() { 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 = "<YOUR_ACCESS_TOKEN>", AccessTokenSecret = "<YOUR_ACCESS_TOKEN_SECRET>", ConsumerKey = "<YOUR_ACCESS_CONSUMER_KEY>", ConsumerSecret = "<YOUR_CONSUMER_SECRET>" }; _client = new Yelp(options); } public YelpController(Yelp client) { _client = client; } 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<string> { "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/developers/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.
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 Autocomplete
plugin.
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 separately. 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
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
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 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.
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.
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.
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).
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 AsideSecond
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 module.
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:
<iframe id="map" scrolling="no" width="300" height="200" frameborder="0" src="@iframeSource">
We also could choose to remove the entire Contrib.Places
field from any summary listing
by updating the Placement.info file for our
template (Figure 5-4):
<Match DisplayType="Summary"> <Place Fields_Contrib_Places="-" /> </Match>
Though we took a few shortcuts, our PlacesField
is a reusable Orchard component. We
could easily share it in other projects simply by copying the project to
another Orchard solution. A better solution, which we’ll learn about in a
later chapter, would be to package it up using the Orchard tools and
submit it to the Orchard gallery (or some private file share).
Content fields are just one type of module we can create in Orchard. In the next chapter, we’re going to create a more complex (in terms of behavior) module, namely a content part that we’ll turn into a widget. Much of what we learned during our field exercise will still apply, but there will be several new concepts to learn.