Universal Windows Apps: a Tale of Two Calendars.

It was the best of times,
it was the worst of times,
it was the age of wisdom,
it was the age of foolishness,
it was the server side calendar on the tablet,
it was the client side calendar on the smart phone.

This article explains two ways of managing appointments in your calendar through a Universal Windows app. I created a small sample MVVM app that hosts a Calendar service with the following functionality against the user’s default calendar:

  • Open the calendar at a specific date and time,
  • create a new appointment,
  • open the calendar at the newly created appointment,
  • delete the newly created appointment,
  • display the number of appointments that were created by the sample app,
  • display the date and time of the appointments that were created by the sample app, and
  • delete all appointments that were created by the sample app.

Here’s a screenshot of it – the orange icons are the buttons that fire the commands:

MainBoth

The Windows Phone part was built upon the Windows 8.1 Appointments API that is elaborated in this article on the Windows App Builder blog: Build apps that connect with People and Calendar, Part 2: Appointments. That API hosts the static AppointmentManager class, which interacts with the user’s Appointments provider app -by default the Calendar app- at the UI level. Unfortunately, most of the AppointmentManager’s methods are only applicable to the phone and cannot be called from a Store app on a tablet or PC. Here are the methods that are called in the phone project of the sample app. The one to show the details of an appointment cannot be used in a Store app, the others are really universal:

  • ShowTimeFrameAsync: displays a time frame from an appointments calendar through the Appointments provider app's primary UI.
  • ShowAddAppointmentAsync: opens the Appointments provider Add Appointment UI, to enable the user to add an appointment.
  • ShowAppointmentDetailsAsync: opens the Appointments provider Appointment Details UI on the specified appointment.
  • ShowRemoveAppointmentAsync: opens the Appointments provider Remove Appointment UI, to enable the user to remove an appointment.

Don’t forget to declare the Appointments capability in the Phone app, or it won’t work. Strangely enough that capability does not exist for Store apps, there you can always use [the supported parts of] this API.

The appointments are not created or removed by the calling app itself, that task is delegated to the Appointments provider. The Appointments provider acts on the local calendar on the device. The local appointment identifiers are passed back to the app, and the appointments themselves are later synced up to the user’s default calendar –most probably Outlook- where they get their ‘real’ identifier (and yes, that’s a different ID).

The API is nice and easy. Here’s the call to open the user’s calendar app at a specific date and time. I embedded it in the Calendar service in the sample app:

public async static Task Open(DateTimeOffset dto, TimeSpan ts)
{
    await AppointmentManager.ShowTimeFrameAsync(dto, ts);
}

Here’s how the ViewModel opens the calendar:

private async void Open_Executed()
{
    await Calendar.Open(DateTimeOffset.Now.AddDays(-7), TimeSpan.FromHours(1));
}

And this is how it looks like on the lightest phone emulator:

OpenPhone

To add or replace an appointment using the Windows 8.1 Appointments API, you need to provide an instance of the Appointment class. Here’s how the ViewModel of the sample app creates one of these:

var appt = new Appointment();
appt.Subject = "Exterminate Enemy";
appt.Details = "Destroy an extraterrestrial race of human-sized pepper shakers, each equipped with a single mechanical eyestalk mounted on a rotating dome, a gun mount containing an energy weapon and a telescopic manipulator arm which is usually tipped by an appendage resembling a sink plunger.";
appt.Location = "That planet on which silence will fall when the oldest question in the universe is asked.";
appt.Invitees.Add(new AppointmentInvitee() { DisplayName = "That impossible girl.", Address = "All around." });
appt.Duration = TimeSpan.FromMinutes(50);
appt.StartTime = DateTimeOffset.Now.AddDays(7);

These are the methods of the sample Calendar service that allow you to add a new appointment. Depending on the device, the Appointments provider will take the whole screen, or appear in a Popup – that’s why you need to provide the Rectangle parameter:

public async static Task<string> Add(Appointment appt)
{
    var selection = new Rect(new Point(Window.Current.Bounds.Width / 2, Window.Current.Bounds.Height / 2), new Size());
    return await Add(appt, selection);
}

public async static Task<string> Add(Appointment appt, Rect selection)
{
    var id = await AppointmentManager.ShowAddAppointmentAsync(appt, selection, Placement.Default);
    AddAppointmentId(id);
    if (String.IsNullOrEmpty(id))
    {
        Toast.ShowInfo("The appointment was not added.");
    }

    return id;
}

These calls return the local identifier of the appointment, which is only valid on the current device. This might be the first show stopper against using this API in your app. Furthermore, the API has no means for querying the calendar – like ‘give me all appointment id’s for appointments that have Time Travel as subject’. So if you require that in your app, please look for another API – there are plenty of them.

I decided to keep the list of identifiers in a semicolon-separated string in the app’s Roaming Settings:

public static void AddAppointmentId(string appointmentId)
{
    if (String.IsNullOrEmpty(appointmentId))
    {
        return;
    }

    string ids = ApplicationData.Current.RoamingSettings.Values["AppointmentIds"] as string;
    if (String.IsNullOrEmpty(ids))
    {
        ids = appointmentId;
    }
    else
    {
        ids += ";" + appointmentId;
    }

    ApplicationData.Current.RoamingSettings.Values["AppointmentIds"] = ids;
}
public static List<string> AppointmentIds
{
    get
    {
        string ids = ApplicationData.Current.RoamingSettings.Values["AppointmentIds"] as string;
        if (String.IsNullOrEmpty(ids))
        {
            return new List<string>();
        }

        return new List<string>(ids.Split(';'));
    }
}

To view or delete an appointment, you need to provide the local identifier. Here’s the code from the Calendar service that opens the Appointment provider on a to-be-deleted appointment. The second parameter enables an On Error Resume Next Whistlingscenario – useful when you want to delete all appointments that were created:

public async static Task Delete(string appointmentId, bool ignoreExceptions = false)
{
    var selection = new Rect(new Point(Window.Current.Bounds.Width / 2, Window.Current.Bounds.Height / 2), new Size());
    try
    {
        var success = await AppointmentManager.ShowRemoveAppointmentAsync(appointmentId, selection);
    }
    catch (Exception)
    {
        if (!ignoreExceptions)
        {
            throw;
        }
    }
}

Here are some screen shots of AppointmentManager calls on the phone emulator – respectively to add, show the details of, and remove an appointment:

PhoneNew SavePhone DeletePhone

Unfortunately the ShowAppointmentDetailsAsync and the ShowEditNewAppointmentAsync calls apply only the phone. You can’t use them in Windows Store apps – although they’re in a Universal API. In some use cases, that’s good enough. If you built a Universal Store app that comes with an accompanying phone app that adds appointment management, then the Windows 8.1 Appointments API is what you need. If you want the exact same functionality on all device types, then you have to look for another API.

The only AppointmentManager call that is really useful in a Windows Store app, is ShowTimeFrameAsync - the call to open the calendar to a specific date and time. This is how the result looks like in the sample app:

StoreOpen

This is how a call to ShowAddAppointmentAsync looks like. Some information of the to-be-inserted appointment appears in a Popup that carries the color scheme of the Calendar app. The only thing you can control is the position of the Popup:

StoreApptMgrUI

So some parts of the Windows 8.1 Appointments API don’t work on a tablet or PC, while other parts simply look ugly on these platforms. Now, there are a lot of alternative appointment API’s available: you can use the Universal AppointmentStore and AppointmentCalendar classes, or the non-universal citizens of the Microsoft.Phone.UserData namespace – including a very promising Appointments class that comes with query capabilities. Unfortunately these API’s have one thing in common: they only apply to the phone.

For the Windows Store app part of the sample app, I decided to directly appeal to the root of the user’s calendar –Outlook.com- and use the Live Connect API for this. This starts with installing the Live SDK, and associating the app with the Store. Just read the first paragraphs of my article on OneDrive integration for more details on this procedure. Since you’re directly talking to the calendar(s) on the server(s), you need to be logged in:

private static async Task LogIn()
{
    if (NetworkInformation.GetInternetConnectionProfile().GetNetworkConnectivityLevel() >= NetworkConnectivityLevel.InternetAccess)
    {
        LiveAuthClient auth = new LiveAuthClient();
        var loginResult = await auth.LoginAsync(new string[] { "wl.calendars", "wl.calendars_update", "wl.events_create" });
        if (loginResult.Session != null)
        {
            client = new LiveConnectClient(loginResult.Session);
        }

        isLoggedIn = (loginResult.Status == LiveConnectSessionStatus.Connected);
    }
    else
    {
        isLoggedIn = false;
    }
}

You also have to know the identifier of the user’s default calendar. Here’s how to find it:

private async static Task<string> FetchCalendarId()
{
    if (!isLoggedIn)
    {
        await LogIn();
    }

    if (client == null || !isLoggedIn)
    {
        return string.Empty;
    }

    try
    {
        LiveOperationResult lor = await client.GetAsync("me/calendars");
        dynamic result = lor.Result;
        foreach (dynamic calendar in result.data)
        {
            // We assume that the first one is the default.
            string id = calendar.id;
            return id;
        }
    }
    catch (Exception ex)
    {
        Debugger.Break();
        return string.Empty;
    }

    return string.Empty;
}

The HTTP POST call to add an appointment takes a dictionary as parameter. So here’s an extension method to format an Appointment instance:

public static Dictionary<string, object> AsDictionary(this Appointment appt)
{
    var calendarEvent = new Dictionary<string, object>();
    calendarEvent.Add("name", appt.Subject);
    calendarEvent.Add("description", appt.Details);
    calendarEvent.Add("start_time", appt.StartTime.ToString("u"));
    calendarEvent.Add("end_time", appt.StartTime.Add(appt.Duration).ToString("u"));
    calendarEvent.Add("location", appt.Location);
    calendarEvent.Add("is_all_day_event", appt.AllDay);

    return calendarEvent;
}

Here’s the full call to add an appointment to the user’s default calendar. It’s not exactly a one-liner: we

  • fetch the calendar id,
  • ensure we’re logged on,
  • add the appointment,
  • store the appointment id in the Roaming Settings,
  • open the local calendar app at the appropriate day, and finally
  • remind the user that he might need to sync it - since we added the appointment on the server-side:
public async static Task<string> Add(Appointment appt)
{
    var calendarId = await FetchCalendarId();

    if (!isLoggedIn)
    {
        await LogIn();
    }

    if (client == null || !isLoggedIn)
    {
        Toast.ShowInfo("Sorry, I could not open your calendar.");
        return string.Empty;
    }

    try
    {
        string parm = string.Format("{0}/events", calendarId);
        LiveOperationResult lor = await client.PostAsync(parm, appt.AsDictionary());
        dynamic result = lor.Result;
        string id = result.id;
        AddAppointmentId(id);
        await Open(appt.StartTime.Date, TimeSpan.FromDays(1));
        Toast.ShowInfo("You may need to sync the calendar.");
        return id;
    }
    catch (Exception ex)
    {
        Debugger.Break();
        return string.Empty;
    }
}

This is how it looks like in the sample app:

StoreToast

When the user taps on the appointment, its details are displayed, with the possibility to edit and delete:

StoreSelect

The Live Connect API comes with an HTTP GET to fetch the appointment details if you know its identifier. Here’s how the sample app fetches the date and time of an appointment:

public async static Task<DateTime> FetchAppointmentDate(string eventId)
{
    if (!isLoggedIn)
    {
        await LogIn();
    }

    if (client == null || !isLoggedIn)
    {
        return DateTime.MinValue;
    }

    try
    {
        LiveOperationResult lor = await client.GetAsync(eventId);
        dynamic result = lor.Result;
        string dt = result.start_time;

        return DateTime.Parse(dt);
    }
    catch (Exception ex)
    {
        // Debugger.Break();
        return DateTime.MinValue;
    }
}

For showing the details of an appointment, I’m only interested in getting its date and time. I use this to open the calendar at the appropriate date:

public async static Task Display(string appointmentId)
{
    var date = await FetchAppointmentDate(appointmentId);
    if (date != DateTime.MinValue)
    {
        await Open(date.Date, TimeSpan.FromDays(1));
    }
    else
    {
        // Not Found.
        Toast.ShowError("Sorry, I could not find the appointment.");
    }
}

Here’s the result in the sample app, this UI should not come as a surprise:

StoreNewAndDisplay

For deleting an appointment, you have two options: open the calendar to the appointment’s date and let the user delete it, or delete it directly with a HTTP DELETE call. Here’s the code for the latter:

public async static Task Delete(string appointmentId, bool ignoreFailure = false)
{
    try
    {
        LiveOperationResult lor = await client.DeleteAsync(appointmentId);
        RemoveAppointmentId(appointmentId);
    }
    catch (Exception ex)
    {
        if (ignoreFailure)
        {
            // On Error Resume Next ...
            RemoveAppointmentId(appointmentId);
        }
        else
        {
            Toast.ShowError("Sorry, something went wrong.");
            Debugger.Break();
        }
    }
}

For the sake of completeness, here’s the shared code that generates the welcome message for Store and Phone apps in the ViewModel. We cannot query for the list of appointments with the used API’s, but we can count the number of identifiers in the Roaming Settings:

        public async Task UpdateMessage()
        {
            message = string.Format("You have {0} appointment(s).", Calendar.AppointmentIds.Count);

#if WINDOWS_PHONE_APP
            this.OnPropertyChanged("Message");
            return;
#else
            foreach (var apptId in Calendar.AppointmentIds)
            {
                var dt = await Calendar.FetchAppointmentDate(apptId);
                if (dt == DateTime.MinValue)
                {
                    message += "\n - (not found)";
                }
                else
                {
                    message += "\n -" + dt.ToString();
                }
            }

            this.OnPropertyChanged("Message");
#endif
        }

Here’s the solution’s structure. The Calendar service is spread over all projects as a partial class (with a large portion in the Shared part), while the XAML of the phone box UI is shared as a user control:

StructureAndManifest

Depending on your use case, you may choose one or the other API for managing appointments in your universal app. But don’t mix them as I did in the sample app. The Windows 8.1 Appointments API works with device-specific identifiers against a local calendar, while the Live Connect API works with global identifiers. The appointments end up in the same place, but there seems to be no way to match the identities.

Here’s the full code of the sample app, it was written with Visual Studio 2013 Update 2. Remember to associate the Windows app with the Store to test the Live Connect API: U2UC.WinUni.Appointments.zip (139.4KB)

Enjoy!

XAML Brewer