Feeding a Companion App with Umbraco
Heads Up!
This article is several years old now, and much has happened since then, so please keep that in mind while reading it.
Overview
Earlier this year, Rock Solid Knowledge were proud to build a brand new Umbraco festival website and associated cross-platform companion app for Gloucester History Festival. The festival is an annual, two-week long celebration of the history of the English Cathedral city of Gloucester. It encompasses hundreds of events including talks, workshops, performances and tours of historic buildings.
When hosting a festival (no mean feat), inevitably you’ll need a website to allow people to browse events, reserve tickets and to find the venues, too - especially if they’re in 15th-century buildings. The festival site allows people to peruse events, search and filter - Examine-backed of course - and allows users to progress to a ticket shop to reserve tickets.
We chose Umbraco CMS for our tech stack. We love it - the flexibility of it, the community involvement, and the beautiful, well-planned UI. It’s like a Companion Cube, letting you pick it up and use it to help you wherever you need, slotting neatly into your dev stack and opening doors for you.
We hosted the website on Azure (following Jeavon Leopold’s excellent guidelines), and launched successfully to deadline with over 10,000 unique users hitting the site since launch. But talking of companions... before this website launched, we already knew we wanted more. More than just a lovely, responsive website. What we wanted was a companion app - a native app for users to download for Android or iOS, to accompany them as they gambolled around Gloucester, helping them to escape using their local device location data when bad signal got them stuck in the cellars of ancient buildings.
For this article, I wanted to share our successful development process of building the Gloucester History Festival website and powering up the companion app with Umbraco, and to provide guidance for those looking to achieve a similar task, utilising Umbraco as a multiple-use content management platform - both for the main website, and as a headless CMS to feed a native app.
I'll cover this:
- How we undertook content modelling in advance to develop a strong data model
- The advantage of writing a clear specification
- The basics of building the web API
- Building the App in Xamarin.Forms
Getting Started
After the initial requirements capture with the client, the creation of a backlog of user stories, and the splitting of functionality into themed fortnightly sprints (we ran this in an agile fashion), it was time to get cracking.
Content Modelling
Initially we undertook domain modelling of data, mapping out the structure of the business domain (a festival) and the relevant data that applied to it. A domain model helps us understand the relationships between domain objects... like a planet and a moon, a guitar and a string, a wheel and a spoke. The relationship between the two is the taxonomy - the “connective tissue” that defines relationships within this network of content objects.
Next, we considered how the content types relate together, whether by inheritance, association or as a whole. We then took this domain model and evolved it into a formalised content model - a representation of structured content with meaning. We undertook content modelling in advance of any development, which allowed us to create a strong collection of content types and their relationships.
Understanding the content model in advance helps lead to a better CMS and author experience, a shared language for businesses to speak, and the definition of a strong data structure. This exercise would ideally be carried out with all key stakeholders, from a product owner to a designer to a business analyst.
One of Umbraco’s key strengths is the ability to carefully define and structure the data as you require by crafting document types. You can then choose to export these document types into strongly-typed models to utilise through code - perhaps by using a models builder, one example being Umbraco’s built-in Models Builder tool (as described in last year’s 24 Days in Umbraco article). This fits neatly with a content modelling approach and means you’re working with tasty strongly-typed models (so many reasons why this is better such as type safety, intellisense and testability).
For Gloucester History Festival, we first modelled the objects and relationships that existed in the festival domain. The “domain universe” of the festival is a high level view of all areas involved. The initial, basic domain model we created was enough to ultimately show the key relationships:
- An event associated with a person
- An event associated with a location
- A festival sponsor, independent to the event itself
After this mapping exercise, we turned these into content models by “zooming in” on the individual areas to start to map out the details of the domain object - including all required attributes - adding the detail required. The example of our Event content model is shown below:
We then converted these into true, Umbracified™ content types. We achieved this by creating the document types in Umbraco (I won’t go into that here - the awesome Umbraco docs explain that well enough already).
We were then able to create an Event as a document type with the required Umbraco-specific properties for the field types. For example, an Image field becomes a Media Picker, a Text field becomes a Text-string and so on:
We repeated this process for all content models and their relevant properties. We structured the site content as follows - note the separation of site content from the global data and site content:
Jonathan Richards has convinced people to follow this path many a time - by separating the presentation content and the shared data into a Site and Global node, the content tree itself has a separation of concerns, and sharing of globally used data becomes far easier in future. Only the content document types need templates - they are used for presentation, whereas the global items are retrieved for data purposes and can be shared between multiple sites and used by various data feeds.
Once we had a strong representation of the content model, we could then rapidly prototype the content model and data into a wireframe, as this Balsamiq example by front-end developer Barnaby Turner shows for an Event:
Specification
Prior to starting development, we wrote a clear API specification. This included documentation of the API requirements and the JSON data structures that we would be returning. The JSON responses were built up from on the content modelling exercise - for example, we knew the object for an Event would include Title, Event Code, Location and so on.
We documented:
- All API methods required
- Expected JSON responses
- Error responses required
- Media headers to use
The clarity of the document prior to build reduced the risk of misunderstandings in terms of API responses required, for example the following section of the document demonstrates the request, expected status codes and the structure of the JSON to be returned
Sample API Specification
Us Umbraco developers took the document and built the API to spec, while the Xamarin devs could start simultaneously writing their Xamarin.Forms delights knowing exactly what we were going to be providing them. No messing.
Web API
Firstly, we recommend installing Postman, or a similar tool, for testing the API responses.
Now, follow along as we describe our approach - you’ve maybe seen this before, but as part of the whole process, I wanted to include the whole #!
- Open Visual Studio 2017 (or your preferred IDE!)
- My personal preference is to create at least a Data and a Web project, in order to separate out the data models and the main website build. It isn’t a must have...though if you do follow this approach...
- ...reference the data project from the Web project
This will initially setup your Umbraco website. Build and run and follow the happy Umbraco installation path of choice (mine’s Nuget).
Now you’re up and running, let us generate the strongly-typed models created from the Umbraco content modelling exercise earlier. This is also personal preference, but I usually install the Umbraco.ModelsBuilder Visual Studio Tool and add the API version to the data project, creating a ModelsBuilder.cs class in the data project to right-click and generate models (after setting up the custom tool). This is all documented thoroughly on the Umbraco.ModelsBuilder Github....but as long as you’re creating strongly-typed models from the content types, your preferred way is fine!
You’ll next need to setup your preferred configuration for your application. Think - what preferences or requirements do you have for your API? Here you can format the JSON output for standards and best practices you require.
In order to setup a custom GlobalConfiguration object, we create a new CustomApplicationEventHandler class that inherits from the ApplicationEventHandler and overrides the ApplicationStarting method. You can then add any custom JSON formatting you require (just ensure there are no ill-effects on any other APIs being used in the project):
using System.Web.Http;
using Umbraco.Core;
public class CustomApplicationEventHandler : ApplicationEventHandler
{
protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
SetupConfig();
}
public void SetupConfig()
{
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Formatters.JsonFormatter.UseDataContractJsonSerializer = false;
}
}
If you would like to use your own routes, rather than the standard /umbraco/api, you will need to ensure that Umbraco routes are overridden.
The way we did this was to create a static WebApiConfig class that will first register the HTTPConfiguration for your API responses, for example adding the supported media types as a custom MediaTypeHeaderValue. Then, the MapHttpAttributeRoutes method will ensure that any of the routes you specify on your API methods (via the Route attribute) will be honoured:
using System;
using System.Net.Http.Headers;
using System.Web.Http;
public class WebApiConfig
{
/// <summary>
/// Register config
/// </summary>
/// <param name="config"></param>
public static void Register(HttpConfiguration config)
{
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+festival-events-v1"));
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+festival-people-v1"));
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+festival-locations-v1"));
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json+festival-branding-v1"));
GlobalConfiguration.Configuration.MapHttpAttributeRoutes();
}
}
Next we created a custom class (call it what you prefer), and inherit from UmbracoApplication, overriding the OnApplicationStarted method. This class should register the custom global configuration you have just created, WebApiConfig:
using System;
using System.Web.Http;
using Umbraco.Web;
public class CustomApplication : UmbracoApplication
{
/// <summary>
/// Overrides the on application started event
/// </summary>
/// <param name="umbracoApplication"></param>
/// <param name="applicationContext"></param>
protected override void OnApplicationStarted(object sender, EventArgs e)
{
base.OnApplicationStarted(sender, e);
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
Make the Umbraco website's global.asax file inherit from this CustomApplication class, instead of the default UmbracoApplication class. Make sure to match the class name exactly:
<%@ Application Inherits="rsk.ghf.umbraco.web.CustomApplication" Language="C#" %>
Now we’re creating our API feed controllers!
API Feed Controllers
We followed Umbraco best practice when building the Web API, naturally. We created a Web API controller that inherited from an UmbracoApiController, as we knew we’d be working with Umbraco data (such as, when the content was last updated) - otherwise we would have probably just built a standard .NET API. Using this Umbraco API approach gave us access to various Umbraco dependencies, like the UmbracoHelper.
Create a new controller in the website project, ensuring that it inherits from the Umbraco.Web.WebApi.UmbracoApiController, and specifying the API route that you require for Umbraco to use, instead of the default Umbraco routing (we overrode this earlier). Here is an example of starting to flesh out the Branding API call, where we gathered branding data from the Site node for the app:
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Umbraco.Web.WebApi;
public class FeedControllerBranding : UmbracoApiController
{
/// <summary>
/// Return the branding information for the site
/// </summary>
/// <param name="changesFrom"></param>
/// <returns></returns>
[HttpGet, Route("api/branding")]
public HttpResponseMessage BrandingResponse([FromUri] string changesFrom = null)
{
return Request.CreateResponse(HttpStatusCode.OK, "Returning branding data");
}
}
We created a method for the API response we wanted, returning a HttpResponseMessage. This is just a sample, so to initially check that this has worked, we have just returned a test response. Now:
- Rebuild everything (and check-in your work so far! Source control for the win!)
- Check that you can hit the API URL you are expecting
- You can also add custom action filter attributes to the method, for example we added one to validate that the "changesfrom" date was formatted correctly, called DateValidatorActionFilterAttribute
In the sample, this will currently just return a default message, of course. What we actually want is some real data from Umbraco.
Controller Returning Umbraco Data
So, next is a sample of the data being retrieved for the location data on the app. When you’re wandering around a city trying to check off as many historical buildings as possible (or even undertaking an Ale tour!), the most important thing you probably want is to know how far away from your next event you are, and what is nearest to your location. As you know from our earlier Event content model, each Gloucester History Festival Event has an associated Location as part of the object.
In our Umbraco instance, a Location model includes a map pin. We created the content type for this property using Jonathan Richard’s lovely Terratype package, which lets us pin the map in Umbraco and get back latitude and longitude (amongst other useful Geo-thingamys), whilst also allowing it to display in Razor templates:
Below is a fairly fleshed-out example of our API method for the latitude/longitude distance calculation for us to find events near the user by querying Umbraco content. Note that we use the excellent model mapping package Our.Umbraco.Ditto (thanks to the collaborators and Umbraco legends Lee Kelleher, Matt Brailsford, James Jackson-South and Rich Green) and the previously mentioned map package Terratype - both can be installed in your preferred way (we used Nuget).
We firstly define DataContracts for our EventsFeed in order to enable the returned data to be serializable. Here are the ones we'll be using in this example, and all should have already been specified in the API documentation. Note the attributes required to make this work:
using System;
using System.Runtime.Serialization;
using System.Globalization;
using Umbraco.Web.Models;
[DataContract]
public class EventEntry
{
[DataMember(Name = "id")]
public Guid ID { get; set; }
[DataMember(Name = "eventCode")]
public string EventCode { get; set; }
[DataMember(Name = "title")]
public string Title { get; set; }
[DataMember(Name = "description")]
public string Description { get; set; }
[DataMember(Name = "heroImage")]
public string HeroImage { get; set; }
[DataMember(Name = "when")]
public EventDateTime[] When { get; set; }
[DataMember(Name = "eventType")]
public int EventType { get; set; }
[DataMember(Name = "additionalImages")]
public string[] AdditionalImages { get; set; }
[DataMember(Name = "tags")]
public string[] Tags { get; set; }
[DataMember(Name = "associatedPeople")]
public Guid[] AssociatedPeople { get; set; }
[DataMember(Name = "location")]
public Guid Location { get; set; }
[DataMember(Name = "meetingPoint")]
public string MeetingPoint { get; set; }
[DataMember(Name = "ticketLink")]
public RelatedLink TicketLink { get; set; }
[DataMember(Name = "price")]
public string Price { get; set; }
[DataMember(Name = "paidEvent")]
public bool PaidEvent { get; set; }
[DataMember(Name = "wheelchairAccessible")]
public bool WheelchairAccessible { get; set; }
[DataMember(Name = "familyFriendly")]
public bool FamilyFriendly { get; set; }
[DataMember(Name = "prebookingRequired")]
public bool PrebookingRequired { get; set; }
[DataMember(Name = "soldOut")]
public bool SoldOut { get; set; }
}
[DataContract]
public class EventDateTime
{
public DateTime FromDateTime { get; set; }
[DataMember(Name = "from")]
public string From { get; set; }
public DateTime ToDateTime { get; set; }
[DataMember(Name = "to")]
public string To { get; set; }
[DataMember(Name = "notes")]
public string Notes { get; set; }
[OnSerializing]
void OnSerializing(StreamingContext context)
{
this.From = this.FromDateTime.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
this.To = this.ToDateTime.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
}
[OnDeserialized]
void OnDeserializing(StreamingContext context)
{
if (this.From == null)
{
this.FromDateTime = default(DateTime);
}
else
{
this.FromDateTime = DateTime.ParseExact(this.From, "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
}
if (this.To == null)
{
this.ToDateTime = default(DateTime);
}
else
{
this.ToDateTime = DateTime.ParseExact(this.To, "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
}
}
}
/// <summary>
/// Events Feed
/// </summary>
[DataContract]
public class EventsFeed
{
[OnSerializing]
void OnSerializing(StreamingContext context)
{
this.ChangesTo = this.ChangesToDateTime.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);
}
public DateTime ChangesToDateTime { get; set; }
[DataMember(Name = "events")]
public EventEntry[] Events { get; set; }
[DataMember(Name = "changesTo")]
public string ChangesTo { get; set; }
}
The sample Events Feed controller code is as follows. Note this code is just for demonstration purposes and you'll need to adjust the logic according to your own requirements, however I've tried to include everything you need for it to work (after you install the required packages). You could also alter this to use Examine to search and return events - especially for high quantities of data and for faster performance - however, the logic that you use to query Umbraco is up to you. This example is predominantly an example to demonstrate how we used Web API to retrieve the data.
We run the logic to get the Umbraco data from various nodes - we reference the Events root node in Umbraco from within the Site node, essentially saying "get the events from here" by using a content picker called "Events Repository". Once we have our Events collection, we can return this with the content type defined as per spec:
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Umbraco.Web.WebApi;
using System.Collections.Generic;
using System.Net.Http.Headers;
using Our.Umbraco.Ditto;
using rsk.ghf.data;
public class FeedController : UmbracoApiController
{
public FeedController()
{
ContentFetcher = new UmbracoContentFetcher(Umbraco);
}
/// <summary>
/// The content fetcher
/// </summary>
public IUmbracoContentFetcher ContentFetcher { get; set; }
/// <summary>
/// Returns events near to the user by device location
/// </summary>
/// <param name="origin"></param>
/// <param name="radius"></param>
/// <param name="hours"></param>
/// <param name="date"></param>
/// <returns></returns>
[HttpGet, Route("api/events/near")]
public HttpResponseMessage GetEventsNearMe([FromUri] string origin, [FromUri]double radius, int hours, [FromUri] DateTime date)
{
if (radius <= 0)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "radius should be a positive number");
}
if (hours <= 0)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "hours should be a positive number");
}
var toDateTime = date.AddHours(hours);
var geoOrigin = origin.ToCoordinate();
var rootNode = ContentFetcher.TypedContentAtRoot().FirstOrDefault() as Site;
if (rootNode != null)
{
var eventsRepo = ContentFetcher.TypedContent(rootNode.EventsRepo.Id) as EventsRepository;
if (eventsRepo != null)
{
List<EventEntry> eventCollection = new List<EventEntry>();
var children = eventsRepo.Children;
foreach (EventModel eventModel in children.As<EventModel>())
{
try
{
foreach (var location in eventModel.Location)
{
if (geoOrigin.GetDistanceTo(location.Map.Position.ToString().ToCoordinate()).ToMiles() <= radius
&& eventModel.DateTimeRange.FirstOrDefault(
x => (x.StartDate >= date && x.StartDate <= toDateTime)
|| x.EndDate >= date && x.EndDate <= toDateTime) != null)
{
eventCollection.Add(CreateEvent(eventModel));
}
}
}
catch (Exception ex)
{
Logger.Error(typeof(FeedController), "\nException creating event: " + ex.Message, ex);
}
}
if (eventCollection.Any())
{
var events = new EventsFeed
{
Events = eventCollection.ToArray(),
};
var response = Request.CreateResponse(HttpStatusCode.OK, events);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json+festival-events-v1");
response.Headers.CacheControl = new CacheControlHeaderValue { NoStore = true };
return response;
}
return Request.CreateResponse(HttpStatusCode.NoContent);
}
}
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, string.Empty);
}
/// <summary>
/// Create Event - fill this out for the entire Event model you require
/// </summary>
/// <param name="eventModel"></param>
/// <param name="rootNode"></param>
/// <returns></returns>
private EventEntry CreateEvent(EventModel eventModel)
{
return new EventEntry
{
ID = eventModel.Id,
EventCode = eventModel.EventCode,
Title = eventModel.EventTitle,
Description = eventModel.Description,
EventType = eventModel.EventType
//TODO: continue mapping in preferred mapping fashion
};
}
}
You might notice that we set the response header content type to the appropriate media type header value (in this example we have "application/json+festival-events-v1"). We also utilise the device data for the location (latitude and longitude), so users didn’t have to rely on having a constantly active data connection for the app to know what their distance to an event was. If you see us at the next Umbraco event, feel free to ask us how we did this behind the scenes!
You'll also need to reference the strongly-typed Umbraco models - I've referenced them here in some fake classes, however the real code should use the strongly-typed models in your preferred fashion (e.g. generated by ModelsBuilder, for example):
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
namespace rsk.ghf.data
{
/// <summary>
/// Replace with real generated Events model
/// </summary>
public class EventModel
{
public IEnumerable<Location> Location { get; internal set; }
public IEnumerable<NcDateTimeRange> DateTimeRange { get; internal set; }
public string EventCode { get; internal set; }
public string EventTitle { get; internal set; }
public string Description { get; internal set; }
public Guid Id { get; internal set; }
public int EventType { get; internal set; }
}
/// <summary>
/// Replace with real generated Location model
/// </summary>
public class Location
{
public Terratype.Models.Model Map
{
get; set;
}
}
/// <summary>
/// Replace with real generated DateTimeRange model
/// </summary>
public class NcDateTimeRange
{
public DateTime StartDate { get; internal set; }
public DateTime EndDate { get; internal set; }
}
/// <summary>
/// Replace with real generated Events Repository model
/// </summary>
public class EventsRepository : PublishedContentModel
{
public EventsRepository(IPublishedContent content) : base(content)
{ }
}
/// <summary>
/// Replace with real generated Site model
/// </summary>
public class Site
{
public IPublishedContent EventsRepo { get; internal set; }
}
}
Also note that we use our ContentFetcher, which wraps the UmbracoContext, to fetch the various content nodes. This content fetcher is instantiated in the controller constructor like so, and aids with unit testing:
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Web;
/// <summary>
/// Umbraco content fetcher to retrieve content via the Umbraco helper
/// </summary>
public class UmbracoContentFetcher : IUmbracoContentFetcher
{
private readonly UmbracoHelper _umbracoHelper;
/// <summary>
/// Instantiate the Umbraco Helper
/// </summary>
/// <param name="umbracoHelper"></param>
public UmbracoContentFetcher(UmbracoHelper umbracoHelper)
{
_umbracoHelper = umbracoHelper;
}
/// <summary>
/// Return typed root node content
/// </summary>
/// <returns></returns>
public IEnumerable<IPublishedContent> TypedContentAtRoot()
{
return _umbracoHelper.TypedContentAtRoot();
}
/// <summary>
/// Return typed content by Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public IPublishedContent TypedContent(int id)
{
return _umbracoHelper.TypedContent(id);
}
}
/// <summary>
/// Interface for the Umbraco Content Fetcher (unit testing, DI)
/// </summary>
public interface IUmbracoContentFetcher
{
IEnumerable<IPublishedContent> TypedContentAtRoot();
IPublishedContent TypedContent(int id);
}
The above code is an example for you to follow, but you’ll no doubt configure this and customise it as you require for your own API requirements and spec.
You now should have the ability to create an Umbraco API controller using custom routes and with the required JSON configuration and response as per spec, along with some examples of Umbraco code you can use inside it!
Xamarin.Forms
Now, the bit you may not have seen before if you’ve not worked in app development. The app was built using Xamarin.Forms by our internal app development team, with the build headed up by Christopher Myhill. And now for the delicious technical details you’re looking for:
- We used version 2.3.4 of Xamarin.Forms
- MVVM Light was used to provide the MVVM architecture (v5.3) - the Model-view-viewmodel pattern
- SQLite was used to provide the data store on the phone
- We used Xamarin.Forms Maps, which provide a cross platform framework for the mapping functionality we required
If you’ve never used or heard of Xamarin.Forms before, it is a way to build native apps for iOS, Android and Windows from a single, shared C# codebase (the language most of Umbraco is written in). Basically put, you can quickly build the app using the Xamarin.Forms controls, and at run-time, each page and control is mapped to platform-specific native UI elements (e.g. different text field types for iOS versus Android).
Xamarin.Forms uses XAML to describe its UI, the same markup originally created by Microsoft to drive their WPF and Windows Phones applications (still in used today in UWP applications). Although the user interface is described using a common language, we can still leverage platform specific look-and-feel. Happily, with recent advancements, native controls from iOS and Android can be brought directly into the XAML using extension methods. Ultimately the use of Xamarin.Forms does not remove access to any native functionality available in iOS or Android… in fact, using Xamarin.Forms allows developers to get an application to market much quicker. It was the key fact that allowed us to get our first version of the companion application created and ready for delivery within 4 weeks!
To access the WebAPI from the Xamarin.Forms project, we used a command pattern:
public class EventsListCommand : ApiCommand<EventItemsResponse>
{
public EventsListCommand() : base(HttpMethod.Get,
"api/events",
"application/json+festival-events-v1")
{
}
public override async Task<EventItemsResponse> Execute()
{
return await RequestHandler.Send(this);
}
public override EventItemsResponse CreateResponse(string rawResponse, HttpStatusCode? statusCode, Exception error = null)
{
return new EventItemsResponse(rawResponse, statusCode, error);
}
}
This was then used by our handler to create a connection to the Umbraco WebAPI. As the API was defined in our specification ahead of time, this meant that a lot of these commands were created and tested before the WebAPI was ready to be called, saving more development time.
Once the data was received we could then bind it to the XAML using the following code (sample of the ListView shown):
<ListView x:Name="eventsList" ItemsSource="{Binding Events}" HasUnevenRows="True"></ListView>
We’re using Umbraco data in Xamarin.Forms. Yum!!!
SUMMARY
While this year’s Gloucester History Festival has now finished, it is back next year, and you can download the free app in both the iOS and Android app-store if you want to have a gander (compare the events data to the website, go on I dare ya.. It’s the same!)
In summary, a combination of data from Umbraco CMS is utilised by both the website and the companion app using Web API. Building the app using Xamarin.Forms helped us to develop a cross-platform app, end-to-end, in only 4 weeks. We will most certainly be doing it again, and as we evolve more ideas of how to combine and mesh the data...you’ll be the first to know.
Emma Garland
Emma is on Twitter as @emmagarland