This article explains how to implement calendar with DHTMLX Scheduler .NET in .NET MVC 5 that uses Outlook Calendar as data source, without using your own database.
After authorization a user will be able to see Outlook Calendar events of his Microsoft Live account. We will use Outlook REST API to interact with calendars and events. It should be noted that all code in this tutorial is valid for MVC 4 application.
For executing methods of Outlook REST we should have access token that is received on successful authorization by OAuth 2.0 protocol. We should set up Windows Live Application and get credentials for authorization in our project.
Note: if we want to get Outlook Calendar events by API, we should select start and end loading date and one of the existing calendars. In this tutorial we will load calendars and offer a user to select one of them. Current month events will be loaded.
Sign in to Windows Live application management site.
1 . You will see a page that contains a list of registered apps. Click Create application on this page (skip this step if you haven't created applications before).
2 . Set an application name (e.g. "Scheduler Demo") and language, agree with the terms.
3 . In the left sidebar open App Settings and copy ClientId and Client secret. We will put them into the configuration file of our application later.
4 . In the left sidebar open API Settings and specify Redirect URL*.
*authorization is done by the OAuth 2.0 protocol. Redirect URL is our application address that matches MVC controller method that receives authorization code from Microsoft server as GET parameter. In our example it will be localhost:58342/Auth/. Note that your port will be different and depends on MVC project properties.
For interaction with Outlook Calendar we will use Outlook REST API, see this link. The scheme of interacting with Outlook Calendar by REST API is the following:
1 . Authorization by the OAuth 2.0 protocol and getting access token.
2 . Using API methods with the received access token.
All logic in the solution will be divided into 3 parts: MVC presentation, Outlook server interaction and OAuth2 authorization. API interaction will be separated from authorization, because it is independent from .NET, while authorization is bound to MVC. So, our solution will contain 3 assemblies:
Here will be data entities and helper class, that we will use for interacting with Outlook REST API.
Outlook REST API returns calendars and events as JSON object called Data. It contains the collection of entities, which we will deserialize into our models below:
Create the Models folder with the following model classes:
public class OutlookCalendar
{
public string Id { get; set; }
public string Name { get; set; }
public string Permissions { get; set; }
}
Â
public class OutlookEvent
{
public string id { get; set; }
public string name { get; set; }
public string description { get; set; }
public DateTime start_time { get; set; }
public DateTime end_time { get; set; }
}
Â
internal class ReceivedCalendarsData
{
public List<OutlookCalendar> Data { get; set; }
}
Â
internal class ReceivedEventsData
{
public List<OutlookEvent> Data { get; set; }
}
Add reference on the system assembly System.Web.Extensions.
Then create the OutlookCalendarApi class in the project root.
public class OutlookCalendarApi
{
public string AccessToken { get; set; }
Â
private const string UrlBase = "https://apis.live.net/v5.0/";
Â
public OutlookCalendarApi()
{
}
Â
public OutlookCalendarApi(string accessToken)
{
AccessToken = accessToken;
}
}
Requests for all Outlook Calendar API methods are similar, that's why we will write a basic request composing method, that sends it and receives a response from the API.
This method uses a generic type of entity to return after getting response from Outlook API server. Data parameter is used for sending event fields for update or create. Here are examples of requests we will compose.
private T GetResponse<T>(string uri, string method, Dictionary<string, object> data)
{
var request = (HttpWebRequest) WebRequest.Create(uri);
request.Method = method;
request.Headers.Add("Authorization", string.Format("Bearer {0}", AccessToken));
Â
if (data.Count != 0)
{
string paramsStr = string.Join("&", data.Select(item => String.Format("{0}={1}", item.Key, item.Value)));
request.ContentLength = paramsStr.Length;
request.ContentType = "application/x-www-form-urlencoded";
Â
using (var sw = new StreamWriter(request.GetRequestStream()))
{
sw.Write(paramsStr);
sw.Close();
}
}
Â
var response = (HttpWebResponse)request.GetResponse();
string responseText;
using (var reader = new StreamReader(response.GetResponseStream()))
{
responseText = reader.ReadToEnd();
}
Â
return new JavaScriptSerializer().Deserialize<T>(responseText);
}
Add the same method for the data parameter with an empty dictionary for simplification
private T GetResponse<T>(string uri, string method)
{
return GetResponse<T>(uri, method, new Dictionary<string, object>());
}
Add methods for getting calendars:
public IEnumerable<OutlookCalendar> GetCalendars()
{
var data = GetResponse<ReceivedCalendarsData>(UrlBase + "me/calendars", "GET");
return data.Data.Where(item => item.Permissions.Equals("owner"));
}
Add events CRUD methods (the calendarId parameter is the id of calendar the selected event belongs or will belong to):
public string CreateEvent(OutlookEvent @event, string calendarId)
{
var sendData = EventAsSendData(@event);
var receivedData = GetResponse<OutlookEvent>(UrlBase + calendarId + "/events", "POST", sendData);
return receivedData.id;
}
Â
public string UpdateEvent(OutlookEvent @event)
{
var sendData = EventAsSendData(@event);
var receivedData = GetResponse<OutlookEvent>(UrlBase + @event.id, "PUT", sendData);
return receivedData.id;
}
Â
public IEnumerable<OutlookEvent> ReadEvents(string calendarId, DateTime from, DateTime to)
{
string requestStr = String.Format(
"{0}{1}/events?start_time={2}Z&end_time={3}Z",
UrlBase,
calendarId,
from.ToString("s"),
to.ToString("s")
);
Â
var eventsData = GetResponse<ReceivedEventsData>(requestStr, "GET");
return eventsData.Data;
}
Â
public void DeleteEvent(string id)
{
GetResponse<object>(UrlBase + id, "DELETE");
}
And a private helper method that we'll use for converting our data entities properties to name-value collection that will be used for request composing:
private Dictionary<string, object> EventAsSendData(OutlookEvent @event)
{
return new Dictionary<string, object>
{
{"name", @event.name},
{"description", @event.description ?? String.Empty},
{"start_time", @event.start_time.ToUniversalTime().ToString("s") + "Z"},
{"end_time", @event.end_time.ToUniversalTime().ToString("s") + "Z"}
};
}
This assembly will contain the auth helper class. OAuth 2.0 authorization implementation is different for each platform/framework, that's why this assembly will contain non-generic MVC authorization.
Authorization process is the following. The user
OAuth 2.0 authorization will be implemented as helper class: MvcAuthHelper.cs. We will authorize each time when we will visit page with calendar in MVC project, because access token expires after 1 hour. So, we will store the access token in cookie.
The algorithm of getting an access token:
So, our MvcAuthHelper.cs will have 3 public methods:
Firstly, install DotNetOpenAuth package from NuGet:
PM> Install-Package DotNetOpenAuth -ProjectName OutlookApi.Authorization.Mvc
After that install .NET MVC package:
PM> Install-Package Microsoft.AspNet.Mvc -Version 5.0.0 -ProjectName OutlookApi.Authorization.Mvc
Then install DotNetOpenAuth MVC5 Extensions package:
PM> Install-Package DotNetOpenAuth.Mvc5 -ProjectName OutlookApi.Authorization.Mvc
Add reference on system assembly System.Web
Create OutlookAuthData.cs file with the following model class:
public class OutlookAuthData
{
public Uri RedirectUri { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public IEnumerable<string> Scopes { get; set; }
}
It contains authorization data, that can be found at Microsoft Live Application. Also, it contains permission scopes for interacting with Outlook REST API, see this link. We will use the following scopes:
Then write MvcAuthHelper class base.
WebServerClient, AuthorizationServerDescription and OutgoingWebResponse in the snippet below are members of the DotNetOpenAuth library, that implements OAuth 2.0 authorization for .NET.
public class MvcAuthHelper : IAuthorizable
{
private readonly HttpContextBase _httpContext;
private readonly OutlookAuthData _authData;
private readonly WebServerClient _client;
private readonly OutgoingWebResponse _response;
public MvcAuthHelper(HttpContextBase httpContext, OutlookAuthData authData)
{
_httpContext = httpContext;
_authData = authData;
var server = new AuthorizationServerDescription
{
AuthorizationEndpoint = new Uri("https://login.live.com/oauth20_authorize.srf"),
TokenEndpoint = new Uri("https://login.live.com/oauth20_token.srf"),
ProtocolVersion = ProtocolVersion.V20
};
_client = new WebServerClient(server, _authData.ClientId, _authData.ClientSecret);
_response = _client.PrepareRequestUserAuthorization(_authData.Scopes, _authData.RedirectUri);
}
}
And the methods mentioned at the beginning of the authorization section are:
public bool IsAuthorized()
{
return !String.IsNullOrEmpty(_httpContext.Request.QueryString["code"]);
}
public object Authorize()
{
return _response.AsActionResultMvc5();
}
public string GetAccessToken()
{
return _client.ProcessUserAuthorization().AccessToken;
}
Creating an empty MVC5 project in Visual Studio 2015 Community is implemented in the following way:
Right click your solution > Add > New Project > Visual C# > Web > ASP.NET Web Application > OK
Then select an Empty template, check MVC checkbox and click OK.
Before adding controllers, let's set a basic layout and styles, install Scheduler.NET package and set needed references.
Create the Content folder with Style.css:
html, body {
font-family: "Arial";
}
.calendar-container {
height: 800px;
}
.display-settings-item {
margin: 20px;
}
.calendar-select-form {
width: 400px;
height: 140px;
margin: 80px auto 0px auto;
padding: 10px;
border: 1px solid lightgray;
text-align: center;
}
Create the Shared folder in Views with _Layout.cshtml file:
<!DOCTYPE html>
Â
<html>
<head>
<title>@ViewBag.Title</title>
<link rel="stylesheet" href="@Url.Content("~/Content/Style.css")"/>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
Add _Viewport.cshtml in the Views folder.
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
Add references on solution projects: OutlookApi.Wrapper and OutlookApi.Authorization.MVC
Install DHTMLX Scheduler.NET using NuGet:
Install-Package DHTMLX.Scheduler.NET -ProjectName Presentation.Mvc5
In MVC application we will have 3 controllers:
Add AuthController to the Controllers folder
/// <summary>
/// Gets Outlook API access token, saves as cookie and redirects
/// to calendar controller
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
var authHelper = new MvcAuthHelper(HttpContext, GetAuthData());
Â
// authorize if not authorized
if (!authHelper.IsAuthorized()) return (ActionResult) authHelper.Authorize();
Â
// sets cookie with token
var tokenCookie = new HttpCookie("OutlookAccessToken", authHelper.GetAccessToken());
tokenCookie.Expires = DateTime.Now.AddHours(1);
HttpContext.Response.SetCookie(tokenCookie);
Â
return RedirectToAction("Select", "Calendar");
}
Â
/// <summary>
/// Gets user's data from config file, needed for authorization by OAuth 2.0 protocol
/// </summary>
/// <returns></returns>
private OutlookAuthData GetAuthData()
{
return new OutlookAuthData
{
ClientId = ConfigurationManager.AppSettings["OutlookApi_ClientId"],
ClientSecret = ConfigurationManager.AppSettings["OutlookApi_ClientSecret"],
RedirectUri = new Uri(ConfigurationManager.AppSettings["OutlookApi_RedirectUri"]),
Scopes = new[] { "wl.calendars_update", "wl.signin", "wl.events_create" }
};
}
The GetAuthData( ) method will take data needed for authorizing by OAuth 2.0 from Web.config AppSettings section, let's add them:
<add key="OutlookApi_RedirectUri" value="[REDIRECT_URL]" />
<add key="OutlookApi_ClientId" value="[CLIENT_ID]" />
<add key="OutlookApi_ClientSecret" value="CLIENT_SECRET" />
Redirect URL, client ID and client secret can be found in your Microsoft Live dev app settings.
Set routing settings in App_Start > RouteConfig.cs, setting AuthControler as the default controller.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Auth", action = "Index", id = UrlParameter.Optional }
);
}
On successful authorization we will get the Select method of Calendar controller, where we will suggest a user to select the calendar to display. So, let's create CalendarController with the Select() method:
public ActionResult Select()
{
// get available calendars for DropDownList
var api = new OutlookCalendarApi(Request.Cookies["OutlookAccessToken"].Value);
var calendars = api.GetCalendars().Select(c => new SelectListItem { Text = c.Name, Value = c.Id }).ToList();
ViewBag.Calendars = calendars;
return View();
}
Then create its view:
@{
ViewBag.Title = "Calendar select page";
Layout = "~/Views/Shared/_Layout.cshtml";
}
Â
<div class="calendar-select-form">
<h2 style="margin-bottom: 40px;">Select calendar</h2>
Â
@using (Html.BeginForm("Index", "Calendar"))
{
<div class="display-settings-item">
@Html.DropDownList("calendarId", (List<SelectListItem>) ViewBag.Calendars)
<button type="submit">Display</button>
</div>
}
</div>
Then add the Index() method to CalendarController, which will initialize and render our calendar:
public ActionResult Index()
{
var scheduler = ConfigureScheduler();
return View(scheduler);
}
private DHXScheduler ConfigureScheduler()
{
var scheduler = new DHXScheduler();
scheduler.LoadData = true;
scheduler.EnableDataprocessor = true;
string calendarId = Request["calendarId"];
// load only this month events
scheduler.EnableDynamicLoading(SchedulerDataLoader.DynamicalLoadingMode.Month);
// compose data action for sending dateFrom, dateTo and accessToken
scheduler.DataAction = String.Format(
"{0}?calendarId={1}",
Url.Action("Data", "DataAccess"),
Url.Encode(calendarId)
);
scheduler.SaveAction = String.Format(
"{0}?calendarid={1}",
Url.Action("Save", "DataAccess"),
Url.Encode(calendarId)
);
scheduler.Lightbox.Add(new LightboxText("text", "Name"));
scheduler.Lightbox.Add(new LightboxText("description", "Details"));
// TODO: implement!
return scheduler;
}
After that add it's view:
@model DHTMLX.Scheduler.DHXScheduler
@{
ViewBag.Title = "Outlook Calendar";
Layout = "~/Views/Shared/_Layout.cshtml";
}
Â
<div class="calendar-container">
@Html.Raw(Model.Render())
</div>
Now add the Event model to the Models folder:
public class Event
{
public string id { get; set; }
public string text { get; set; }
public string description { get; set; }
public DateTime start_date { get; set; }
public DateTime end_date { get; set; }
}
DataAccess controller will have 2 public methods:
public ActionResult Data()
{
var api = new OutlookCalendarApi
{
AccessToken = Request.Cookies["OutlookAccessToken"].Value
};
Â
var events = api.ReadEvents(
Request["calendarId"],
DateTime.Parse(Request.QueryString["from"]),
DateTime.Parse(Request.QueryString["to"])
);
Â
return new SchedulerAjaxData(events.Select(item => new Event
{
id = item.id,
text = item.name,
description = item.description,
start_date = item.start_time,
end_date = item.end_time
}));
}
Â
public ActionResult Save(string id, FormCollection actionValues)
{
var action = new DataAction<string>(actionValues);
var @event = DHXEventsHelper.Bind<Event>(actionValues);
Â
try
{
var api = new OutlookCalendarApi
{
AccessToken = Request.Cookies["OutlookAccessToken"].Value
};
Â
switch (action.Type)
{
case DataActionTypes.Insert:
action.TargetId = api.CreateEvent(EventToOutlookEvent(@event), Request["calendarId"]);
break;
case DataActionTypes.Update:
api.UpdateEvent(EventToOutlookEvent(@event));
break;
case DataActionTypes.Delete:
api.DeleteEvent(id);
break;
}
}
catch
{
action.Type = DataActionTypes.Error;
}
Â
return new AjaxSaveResponse(action);
}
Â
private OutlookEvent EventToOutlookEvent(Event @event)
{
return new OutlookEvent
{
description = @event.description,
name = @event.text,
start_time = @event.start_date,
end_time = @event.end_date,
id = @event.id
};
}