Cloud Services Embedded Example 2: Resume an Interview
When users load an interview, it is often useful if they can exit the interview and return to it again later, in the same state it was when they stopped. In the Cloud Services Embedded API, Interview Snapshots allow a user to save the state of an interview and resume it later.
In this example, you will:
- Calculate an HMAC to authorize the Resume Session request
- Retrieve a saved Interview Snapshot
- Create and send a ResumeSession request
- Get a Session ID from the response
- Generate the embedded interview, using the Session ID and Interview Snapshot
Full source code for this example is available at the bottom of the page.
Example Source Code on GitHub
The CloudServicesEmbeddedAPIExample example project is available on GitHub, in the HotDocs-Samples repository.
Prerequisites
Before using this walkthrough, you should complete Cloud Services Embedded Interview Example 1: Display an Interview.
1. Open the MVC Application Project from Example 1
To begin, open the CloudServicesEmbeddedAPIExample project created in Example 1. You will extend this example to allow saving and restoring interview state.
2. Save an Interview Snapshot
An Interview Snapshot is a saved interview state string. This records the state of the interview and allows for an interview and its answers to be recreated. In this example, you will save the Interview Snapshot in a text file. You could also save the snapshot in a database. In this section, you will:
- Add a method to save the Snapshot
- Add a Controller method to access the save snapshot method
- Add a button to the interview page for the user to save an interview snapshot
2.1 Add a SaveSnapshot method
In the EmbeddedInterview.cs file, create a new method, SaveSnapshot:
public static void SaveSnapshot(string snapshot)
{
}
To save the Interview Snapshot string to a text file, add the following line:
System.IO.File.WriteAllText(@"C:\temp\snapshot.txt", snapshot);
The full method looks as follows:
public static void SaveSnapshot(string snapshot)
{
System.IO.File.WriteAllText(@"C:\temp\snapshot.txt", snapshot);
}
2.2 Add a Controller Action to Save the Snapshot
In the HomeController.cs file, create a new Controller method, SaveSnapshot:
public void SaveSnapshot(string snapshot) { EmbeddedInterview.SaveSnapshot(snapshot); }
This method calls the SaveSnapshot method created in the previous step.
2.3 Add a Save Snapshot button to the Interview page
To allow the user to take an interview snapshot and save it to disk, you must add a button to the interview page. To do this,
- In the project, navigate to Views > Home
- Open the Index.cshtml file
Next, add the following HTML to the top of the page, inside the body tag:
<button onclick="snapshot()">Save Interview Snapshot</button>
In the next step, you will add the JavaScript code required to save the snapshot.
2.4 Add the Save Snapshot JavaScript
At the bottom of the Index.cshtml page, add Script tags:
<script>
</script>
Inside the Script tags, add the following JavaScipt function:
function snapshot() {
HD$.GetSnapshot(
function (sessionSnapshotData) {
$.post("http://localhost/examplehostapplication/Home/SaveSnapshot/", { snapshot: sessionSnapshotData });
}
);
}
This function uses the HotDocs Embedded JavaScript GetSnapshot function to pass the Session Snapshot Data to another function.
The function inside HD$.GetSnapshot is our own callback function. This function posts back the retrieved Session Snapshot Data to an endpoint in the example host application. In this case, the endpoint is the SaveSnapshot Controller method created above.
The save snapshot functionality is now ready to test.
3. Test Saving an Interview Snapshot
To test saving an Interview Snapshot:
- Set the current project as the Startup Project (Right-click the CloudServiceEmbeddedAPIExample project in Visual Studio and select Startup Project from the drop-down menu)
- Press F5 to run the Project. The interview loads.
- Enter answer data into the interview.
- Click the Save Interview Snapshot button.
- Navigate to the location of the saved snapshot text file, C:\temp\.
- A snapshot.txt file appears in the directory.
4. Restore an Interview from a Snapshot
To restore an interview from a snapshot, you need to pass the snapshot data to the Cloud Services ResumeSession method. The method returns a Session ID from which an interview can be loaded, containing the answers from the saved session.
4.1 Get the Interview Snapshot
In the steps above, you created a method to save the interview snapshot data to disk as a text file. In this step, you will retrieve the text file from disk and read the contents.
In EmbeddedInterview.cs, create a new method, GetSnapshot:
private static string GetSnapshot(){ }
Add the following line to read the contents of the snapshot file and return it as a string:
return System.IO.File.ReadAllText(@"C:\temp\snapshot.txt");
The full method looks as follows:
private static string GetSnapshot() { return System.IO.File.ReadAllText(@"C:\temp\snapshot.txt"); }
4.2 Create the Resume Interview Request
To resume an interview using the interview snapshot, you must send a request to the Cloud Services Resume Session method. To do this, create a new method, CreateResumeSessionHttpRequestMessage:
4.2.1 Create the request method
private static HttpRequestMessage CreateResumeSessionHttpRequestMessage(string hmac, string subscriberId, string packageId, string snapshot, DateTime timestamp)
{
}
This method is similar to the CreateHttpRequest method created in the previous example. However, the parameters required for the ResumeSession request are slightly different. These parameters are:
- hmac – the HMAC calculated in the step above.
- subscriberId – your ID for signing in to Cloud Services.
- packageId – the name of the Template Package used for the assembly.
- snapshot – the interview snapshot data, used to recreate the session
- timestamp – the time at which the assembly was started.
4.2.2 Create the request URL
Next, create the request URL. The URL for ResumeSession has the following format:
http://{endpoint}/embed/resumesession/{subscriberID}/{packageID}?date={timestamp}
The placeholders values in this URL are:
- endpoint – the appropriate Cloud Services instance for your account. Either:
-
- USA – cloud.hotdocs.ws
- EU – europe.hotdocs.ws
See Cloud Services REST API Endpoints for more information.
- subscriberID – your ID for signing in to Cloud Services
- packageID – the name of the Template Package used for the assembly
- timestamp – the date and time of the request
Add the following line to the CreateHttpRequestMessage method:
var resumeSessionUrl = string.Format("https://cloud.hotdocs.ws/embed/resumesession/{0}/{1}?date={2}", subscriberId, packageId, timestamp);
4.2.3 Create the request message
Next, add the request message, using the URL created above as the request uri and the snapshot as the request content:
var request = new HttpRequestMessage
{
RequestUri = new Uri(resumeSessionUrl),
Method = HttpMethod.Post,
Content = new StringContent(snapshot)
};
4.2.4 Add the request headers
Finally, add the following headers to the request:
request.Headers.Add("x-hd-date", timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ"));
request.Content.Headers.Remove("Content-Type");
request.Content.Headers.Add("Content-Type", "text/plain");
request.Headers.TryAddWithoutValidation("Authorization", hmac);
request.Headers.Add("Keep-Alive", "false");
The request can now be returned.
4.2.5 Complete CreateResumeSessionHttpRequestMessage method
The complete CreateResumeSessionHttpRequestMessage method looks as follows:
private static HttpRequestMessage CreateResumeSessionHttpRequestMessage(string hmac, string subscriberId, string packageId, string snapshot, DateTime timestamp, string interviewFormat, string outputFormat) { var resumeSessionUrl = string.Format("https://cloud.hotdocs.ws/embed/resumesession/{0}/{1}?date={2}", subscriberId, packageId, timestamp); var request = new HttpRequestMessage { RequestUri = new Uri(resumeSessionUrl), Method = HttpMethod.Post, Content = new StringContent(snapshot) }; request.Headers.Add("x-hd-date", timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); request.Content.Headers.Remove("Content-Type"); request.Content.Headers.Add("Content-Type", "text/plain"); request.Headers.TryAddWithoutValidation("Authorization", hmac); request.Headers.Add("Keep-Alive", "false"); return request; }
5. Create the ResumeCloudServicesSession method
Now that you have created method to retrieve the snapshot and create the request message, you can create a method that uses these to send the Resume Session request to Cloud Services and return the response.
5.1 Create the method
First, create a new method, ResumeCloudServicesSession:
public static string ResumeCloudServicesSession()
{
}
This method will send the Resume Session request and return a Session ID.
5.2 Get the Interview Snapshot
Next, use the GetSnapshot method to retrieve the interview snapshot string from disk:
var snapshot = GetSnapshot();
5.3 Calculate the HMAC
Use the CalculateHMAC method to calculate the HMAC for the request. The calculation for ResumeSession requires the following parameters:
- signingKey (string) – the unique key associated with your user account, used for signing requests to Cloud Services.
- timestamp (DateTime) – the time at which the assembly was started. This is used by Cloud Services to check that an assembly request has not expired.
- subscriberId (string) – your ID for signing in to Cloud Services.
- snapshot – the interview snapshot string retrieved in the previous step.
Add the following line to the method:
var hmac = CalculateHMAC(signingKey, timestamp, subscriberId, snapshot);
5.4 Create the Request
Next, create the request message using the CreateResumeSessionHttpRequestMessage method:
var request = CreateResumeSessionHttpRequestMessage(hmac, subscriberId, packageId, snapshot, timestamp);
5.5 Send the Request to Cloud Services
Now that the request message is created, it must be sent to Cloud Services. Use HttpClient to send the request:
var client = new HttpClient();
var response = client.SendAsync(request);
response.Wait();
As the request is asynchronous, you must wait for the response to be calculated before it can be used in the next step.
5.6 Get the Session ID from the Response
To display the interview, you need to pass the Session ID from the response to the HotDocs JavaScript. Read the Session ID from the response Content:
var responseContentStream = response.Result.Content;
var sessionId = responseContentStream.ReadAsStringAsync().Result;
The Session ID can then be returned.
5.7 Complete ResumeCloudServicesSession method
The completed ResumeCloudServicesSession method looks as follows:
public static string ResumeCloudServicesSession() { var snapshot = GetSnapshot(); var hmac = CalculateHMAC(signingKey, timestamp, subscriberId, snapshot); var request = CreateResumeSessionHttpRequestMessage(hmac, subscriberId, packageId, snapshot, timestamp); var client = new HttpClient(); var response = client.SendAsync(request); response.Wait(); var responseContentStream = response.Result.Content; var sessionId = responseContentStream.ReadAsStringAsync().Result; return sessionId; }
6. Make an Embedded Interview Request from the Home Controller
To display the embedded interview, you will need to make a request to Cloud Services from the Home Controller, which can then pass the retrieved Session ID to a View, using the InterviewModel. The View will render the interview for the user.
6.1 Making the Embedded Interview Request
- In the CloudServicesEmbeddedAPIExample project, navigate to Controllers > HomeController.
- Edit the HomeController.cs file
In the HomeController file, create a new ResumeSession ActionResult method.
public ActionResult ResumeSession()
{
}
In this method, first create a new InterviewModel:
InterviewModel interviewModel = new InterviewModel
{
SessionId = EmbeddedInterview.ResumeCloudServicesSession()
};
The Session ID in the model is populated by a call to the ResumeCloudServicesSession method created earlier.
Next, pass the model to the View:
return View("Index", interviewModel);
6.2 Complete ResumeSession ActionResult Method
The complete ResumeSession method looks like this:
public ActionResult ResumeSession() { InterviewModel interviewModel = new InterviewModel { SessionId = EmbeddedInterview.ResumeCloudServicesSession() }; return View("Index", interviewModel); }
The resume session functionality is now ready to test.
Testing
To test creating an interview using Cloud Services:
- Set the current project as the Startup Project (Right-click the CloudServicesEmbeddedAPIExample project in Visual Studio and select Startup Project from the drop-down menu)
- Press F5 to run the Project. The interview loads in the web browser.
- Edit the URL in the browser to go to the Home Controller Resume Session method, e.g. http://{machineName}/{hostApplication}/Home/ResumeSession
- An interview loads containing the answers from the snapshot.
Source Code (C#)
EmbeddedInterview.cs
using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; namespace CloudServicesEmbeddedAPIExample { public class EmbeddedInterview { // Subscriber Information static readonly string subscriberId = "example-subscriber-id"; static readonly string signingKey = "example-signing-key"; // HMAC calculation data static readonly DateTime timestamp = DateTime.UtcNow; static readonly string packageId = "ed40775b-5e7d-4a51-b4d1-32bf9d6e9e29"; static readonly string billingRef = "ExampleBillingRef"; static readonly string interviewFormat = "JavaScript"; static readonly string outputFormat = "DOCX"; static readonly bool showDownloadLinks = true; static readonly string settings = null; public static string CreateCloudServicesSession() { // Generate HMAC using Cloud Services signing key var hmac = CalculateHMAC(signingKey, timestamp, subscriberId, packageId, billingRef, interviewFormat, outputFormat, settings); // Create Session request var request = CreateHttpRequestMessage(hmac, subscriberId, packageId, timestamp, interviewFormat, outputFormat, showDownloadLinks, billingRef); //Send upload request to Cloud Service var client = new HttpClient(); var response = client.SendAsync(request); response.Wait(); // Get Session ID from Response Content Stream var responseContentStream = response.Result.Content; var sessionId = responseContentStream.ReadAsStringAsync().Result; return sessionId; } public static string ResumeCloudServicesSession() { // Get the Interview Snapshot from disk var snapshot = GetSnapshot(); // Generate HMAC using Cloud Services signing key var hmac = CalculateHMAC(signingKey, timestamp, subscriberId, snapshot); // Create Session request var request = CreateResumeSessionHttpRequestMessage(hmac, subscriberId, packageId, snapshot, timestamp); //Send Create Session request to Cloud Service var client = new HttpClient(); var response = client.SendAsync(request); response.Wait(); // Get Session ID from Response Content Stream var responseContentStream = response.Result.Content; var sessionId = responseContentStream.ReadAsStringAsync().Result; return sessionId; } private static HttpRequestMessage CreateHttpRequestMessage(string hmac, string subscriberId, string packageId, DateTime timestamp, string interviewFormat, string outputFormat, bool showDownloadLinks, string billingRef) { var newSessionUrl = string.Format("https://cloud.hotdocs.ws/embed/newsession/{0}/{1}?interviewFormat={2}&outputformat={3}", subscriberId, packageId, interviewFormat, outputFormat&showdownloadlinks={4}&billingRef={5}", subscriberId, packageId, interviewFormat, outputFormat, showDownloadLinks, billingRef); var request = new HttpRequestMessage { RequestUri = new Uri(newSessionUrl), Method = HttpMethod.Post, Content = GetAnswers() }; // Add request headers request.Headers.Add("x-hd-date", timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); request.Headers.TryAddWithoutValidation("Authorization", hmac); request.Headers.Add("Keep-Alive", "false"); request.Headers.TryAddWithoutValidation("Content-Type", "text/xml"); return request; } private static HttpRequestMessage CreateResumeSessionHttpRequestMessage(string hmac, string subscriberId, string packageId, string snapshot, DateTime timestamp) { var resumeSessionUrl = string.Format("https://cloud.hotdocs.ws/embed/resumesession/{0}/{1}?date={2}", subscriberId, packageId, timestamp); var request = new HttpRequestMessage { RequestUri = new Uri(resumeSessionUrl), Method = HttpMethod.Post, Content = new StringContent(snapshot) }; // Add request headers request.Headers.Add("x-hd-date", timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ")); request.Content.Headers.Remove("Content-Type"); request.Content.Headers.Add("Content-Type", "text/plain"); request.Headers.TryAddWithoutValidation("Authorization", hmac); request.Headers.Add("Keep-Alive", "false"); return request; } public static void SaveSnapshot(string snapshot) { if (snapshot != null) { System.IO.File.WriteAllText(@"C:\temp\snapshot.txt", snapshot); } } public static string GetSnapshot() { if (System.IO.File.Exists(@"C:\temp\snapshot.txt")) { return System.IO.File.ReadAllText(@"C:\temp\snapshot.txt"); } return ""; } private static StringContent GetAnswers() { return new StringContent(@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?><AnswerSet version=""1.1""><Answer name=""TextExample-t""><TextValue>Hello World</TextValue></Answer></AnswerSet >"); } public static string CalculateHMAC(string signingKey, params object[] paramList) { byte[] key = Encoding.UTF8.GetBytes(signingKey); string stringToSign = Canonicalize(paramList); byte[] bytesToSign = Encoding.UTF8.GetBytes(stringToSign); byte[] signature; using (var hmac = new System.Security.Cryptography.HMACSHA1(key)) { signature = hmac.ComputeHash(bytesToSign); } return Convert.ToBase64String(signature); } public static string Canonicalize(params object[] paramList) { if (paramList == null) { throw new ArgumentNullException(); } var strings = paramList.Select(param => { if (param is string || param is int || param is Enum || param is bool) { return param.ToString(); } if (param is DateTime) { DateTime utcTime = ((DateTime)param).ToUniversalTime(); return utcTime.ToString("yyyy-MM-ddTHH:mm:ssZ"); } if (param is Dictionary<string, string>) { var sorted = ((Dictionary<string, string>)param).OrderBy(kv => kv.Key); var stringified = sorted.Select(kv => kv.Key + "=" + kv.Value).ToArray(); return string.Join("\n", stringified); } return ""; }); return string.Join("\n", strings.ToArray()); } } }
HomeController.cs
using System.Web.Mvc; using CloudServicesEmbeddedAPIExample.Models; namespace CloudServicesEmbeddedAPIExample.Controllers { public class HomeController : Controller { public ActionResult Index() { InterviewModel interviewModel = new InterviewModel { SessionId = EmbeddedInterview.CreateCloudServicesSession() }; return View(interviewModel); } public ActionResult ResumeSession() { InterviewModel interviewModel = new InterviewModel { SessionId = EmbeddedInterview.ResumeCloudServicesSession() }; return View("Index", interviewModel); } public void SaveSnapshot(string snapshot) { EmbeddedInterview.SaveSnapshot(snapshot); } } }
InterviewModel.cs
namespace CloudServicesEmbeddedAPIExample.Models
{
public class InterviewModel
{
public string SessionId { get; set; }
}
}
Index.cshtml
@model CloudServicesEmbeddedAPIExample.Models.InterviewModel <body onload="HD$.CreateInterviewFrame('InterviewContainer', '@Model.SessionId'); "> <form id="form1" runat="server"> <button onclick="snapshot()">Save Interview Snapshot</button> <h1>Employment Agreement Generator</h1> <div id="InterviewContainer" style="width:100%; height:600px; border:1px solid black"></div> </form> <script> /* Save an Interview Snapshot */ function snapshot() { HD$.GetSnapshot(function (sessionSnapshotData) { $.post("http://localhost/CloudServicesEmbeddedAPIExample/Home/SaveSnapshot/", { snapshot: sessionSnapshotData }); }); } </script> </body>