Cloud Services Example 3: Display an Interview

A core function of HotDocs is the Interview, a series of questions displayed to users in order to gather answer data that HotDocs can use to assemble a completed document. You can use Cloud Services to provide a fragment of HTML that allows you to embed the HotDocs Interview in a web page, where users can interact with it.

This example takes you through the process of creating an interview and retrieving the HTML fragment used to display the interview to users.

In this example you will:

  • Create a new MVC project in Visual Studio
  • Generate a HotDocs Interview HTML fragment using Cloud Services
  • Use the HTML fragment to display a HotDocs Interview to the user

Full source code for this example is available at the bottom of the page.

Example Source Code on GitHub

The CloudServicesAPIExample3Interview example project is available on GitHub, in the HotDocs-Samples repository.

Requirements

Displaying an interview in Cloud Services requires that a HotDocs Template Package File is uploaded to the service. For information on uploading a Template Package File, see Example 1: Uploading a Template.

1. Create a new MVC Project in Visual Studio

Before you start, create a new MVC Application project for this example named CloudServicesAPIExample3Interview. You will gradually add code to this project until you have a working example.

2. Deploy the HotDocs Interview Runtime Files

2.1 Download the latest Interview Runtime Files

See the download links in the Browser Interview Release Notes to get the latest version of the Interview Runtime Files.

2.2 Copy the Files

  1. Extract the downloaded Interview Runtime Files zip file.
  2. Navigate to C:\Inetpub\wwwroot and create a new folder, HDInterviewFiles.
  3. Copy the scripts and stylesheets folders to C:\Inetpub\wwwroot\HDInterviewFiles.

2. Reference the DLLs

In the project, edit Program.cs. Add the following using statements at the beginning of the file:

  • using System.Net.Http;
  • using System.Text;
  • using System.Threading;
  • using System.Threading.Tasks;
  • using System.Web;
  • using System.Drawing

3. Create a new Interview Request class

To retrieve the required files and render the interview, first create a new class, InterviewRequest:

  1. In Visual Studio, right-click the CloudServicesAPIExample3Interviewproject and select Add > Class.
  2. Enter InterviewRequest.cs in the Name field.
  3. Click OK. The new InterviewRequest class is added to the project.

You will now add methods to this class to request the interview.

4. Add Subscriber ID and Other Information

Edit the InterviewRequest class and create a new method, GetInterviewResponse:

private HttpResponseMessage GetInterviewResponse()
{
}

When it is complete, this method will connect to Cloud Services, send the interview request, and receive the response message.

4.1 Add Subscriber Information

In the GetInterviewResponse method of the class, add variables with your Cloud Services Subscriber information:

var subscriberId = "example-subscriber-id";
var signingKey = "example-signing-key"; 

These items are:

  • Subscriber ID – your ID for signing in to Cloud Services. This is typically the name of your organisation.
  • Signing Key – the unique key associated with your user account. This is used for signing requests made to Cloud Services.

The Subscriber ID and Signing Key are provided when you sign up for Cloud Services. If you do not have this information, please contact your HotDocs Sales Representative.

You will use these items later when calculating the HMAC and creating the HTTP request to access Cloud Services.

4.2 Add Data Required to Retrieve the Interview

Next, add a Package ID variable:

var packageId = "ed40775b-5e7d-4a51-b4d1-32bf9d6e9e29";

This is the unique ID (in this case, a GUID) that identifies the HotDocs Template Package File used for the interview request.

Add the required Interview Format:

var format = "JavaScript";

The format can be:

  • JavaScript – the interview is rendered using JavaScript.
  • Silverlight – the interview is rendered using Silverlight.
  • Native – selects an interview format based on the user's browser

Add the Temporary Image URL:

var tempImageUrl = "http://localhost/HDInterviewFiles/";

This is the location where image files are temporarily stored when required by the interview. The HDInterviewFiles IIS application must use the physical path to which interview files are saved, i.e. C:\temp\

You will use these items later when retrieving the interview from Web Services.

 

In the GetInterview method, add the following seven variables and values:

var timestamp = DateTime.UtcNow;
var templateName = "";
var sendPackage = false;
var billingRef = "ExampleBillingRef";

These items are:

  • Timestamp (DateTime) – the time at which the interview was started. This is used by Cloud Services to check that an interview request has not expired.
  • Template Name (string) – may be empty when assembling a document, as the template name is the same as the package file name.
  • Send Package (boolean) – Must be false when generating an interview. Indicates that a Template Package is not being sent with a request.
  • Billing Reference (string) – a custom logging message. This is typically used to identify the user making the request, for billing purposes.

Finally, add the required interview settings:

var settings = new Dictionary<string, string>
{
        {"HotDocsJsUrl", "https://localhost/HDInterviewFiles/scripts/"},
        {"HotDocsCssUrl", "https://localhost/HDInterviewFiles/stylesheets/hdsuser.css"},
        {"InterviewDefUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewDefinition/"},
        {"SaveAnswersPageUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/SaveAnswers/"},  
        {"FormActionUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewFinish"}
};

The Interview Request requires five interview settings:

  • HotDocsJsUrl – the URL where the HotDocs JavaScript files are located. This is the URL for the Interview Runtime Files scripts directory, served from your own domain.
  • HotDocsCssUrl – the URL for the HotDocs Server Interview CSS file, hdsuser.css. This file is located in the Interview Runtime Files Stylesheets directory, served from your own domain. Additional interview files are downloaded from the base URL (e.g. https://localhost/HDInterviewFiles/stylesheets/).
  • InterviewDefUrl – the URL for the HotDocs Template JavaScript file. When using Cloud Services, this file must be downloaded and served from the same domain as the host application.
  • SaveAnswersPageUrl – the URL to which answers can be saved during an interview.
  • FormActionUrl – the URL of the page to which the user is sent when the click the Finish button in the interview. Typically, this is a page from which the user can download the assembled document.

You will use these items later when calculating the HMAC and creating the HTTP request to access Cloud Services.

5. Calculate the HMAC

Every request made to Cloud Services must use an HMAC to verify the request. To calculate the HMAC, you must add two new methods to Program.cs:

  • CalculateHMAC – performs the HMAC calculation and returns the HMAC string.
  • CanonicalizeParameters – takes the parameters for the request and converts them into the standardised format required to calculate the HMAC.

5.1 Create the CalculateHMAC method

To calculate the HMAC, add a new method, CalculateHMAC:

public string CalculateHMAC(string signingKey, params object[] parametersList)
{
        var key = Encoding.UTF8.GetBytes(signingKey);
        var parameterStringToSign = Canonicalize(parametersList);
        var parameterBytesToSign = Encoding.UTF8.GetBytes(parameterStringToSign);
        byte[] signature;
        using (var hmac = new System.Security.Cryptography.HMACSHA1(key))
        {
                signature = hmac.ComputeHash(parameterBytesToSign);
        }
        return Convert.ToBase64String(signature);
}

This method calculates the HMAC by:

  • Converting the signing key to a byte array
  • Canonicalizing the parameters
  • Converting the canonicalized parameters to a byte array
  • Creating a new HMAC using the converted signing key and converted canonicalized parameters.
  • Returning the new HMAC signature as a string.

For this method to work, you must create a second method to canonicalize the parameters used to create the HMAC, before the HMAC is calculated.

5.2 Create the Canonicalize method

The canonicalize method takes all of the parameters required to calculate the HMAC, converts them to appropriately-formatted strings, and joins the strings together. The complete method looks as follows:

public 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());
}  

5.3 Retrieve the Calculated HMAC

Add the following line to the GetInterviewResponse method, to retrieve the calculated HMAC:

var hmac = CalculateHMAC(signingKey, timestamp, subscriberId, packageId, templateName, sendPackage, billingRef, format, tempImageUrl, settings);

To calculate the HMAC for displaying an interview, you must pass in 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.
  • packageId (string) –  the name of the Template Package used for the assembly.
  • templateName (string) – may be empty when assembling a document, as the template name is the same as the package file name.
  • sendPackage (boolean) – Must be false when assembling a document. Indicates that a package is being sent with a request.
  • billingRef (string) – a custom logging message. This is typically used to identify the user making the request, for billing purposes.
  • format (string) – the format of the assembled document.
  • settings (string array) – You can also optionally include on the query string any document assembly settings from those listed in AssembleDocumentOptions.Settings. Settings specified on the query string will override the defaults designated in the Cloud Services administration portal.

Once the HMAC has been calculated, you will use it when sending the GetInterview request to Cloud Services.

6. Create the GetInterview request

In this step, you will add a new method to create a request message. The message contains the information required to make a Get Interview request to Cloud Services.

6.1 Create the CreateHttpRequestMessage method

Create a new method, CreateHttpRequestMessage:

private HttpRequestMessage CreateHttpRequestMessage(string hmac, string subscriberId, string packageId, DateTime timestamp, string billingRef, string format, string tempImageUrl, Dictionary<string, string> settings)
{
}

This method takes the following parameters:

  • hmac – the HMAC calculated in the step above
  • timestamp – the time at which the assembly was started
  • subscriberId – your ID for signing in to Cloud Services
  • packageId – the name of the Template Package used for the assembly
  • templateName – the name of the Template in the Template Package used to create the interview
  • sendPackage – always false when using GetInterview; tells Cloud Services if a package is uploaded with the request
  • billingRef a custom logging message
  • format – the format of the assembled document
  • tempImageUrl – the URL of the endpoint in the host application from which interview image files are retrieved
  • settings – any additional assembly settings used

6.2 Create the Get Interview URL

The URL for the GetInterview request uses the following format:

http://{endpoint}/hdcs/interview/{subscriberID}/{packageID}?billingRef={billingRef}&format={format}&tempimageurl={tempImageUrl}

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
  • format – the format of the assembled document
  • tempImageUrl – the URL of the endpoint in the host application from which interview image files are retrieved

Add the following line to the CreateHttpRequestMessage method:

var partialInterviewUrl = string.Format("https://cloud.hotdocs.ws/hdcs/interview/{0}/{1}?billingRef={2}&format={3}&tempimageurl={4}", subscriberId, packageId, billingRef, format, tempImageUrl);

However, the interview settings must also be added to the URL. To do this, you will iterate through the settings collection and append the appropriate attributes to the URL.

First, use the partialInterviewUrl to create a new StringBuilder:

var completedInterviewUrlBuilder = new StringBuilder(partialInterviewUrl);

Next, create a foreach loop to iterate through the settings collection:


foreach (var kv in settings) { }

For each key-value pair in settings, append the key and value to the interview URL as query string parameters:


completedInterviewUrlBuilder.AppendFormat("&{0}={1}", kv.Key, kv.Value ?? "");            

The completed loop looks as follows:


foreach (var kv in settings) {                 completedInterviewUrlBuilder.AppendFormat("&{0}={1}", kv.Key, kv.Value ?? "");
}

Finally, convert the completedInterviewUrlBuilder to a string:

var InterviewUrl = completedInterviewUrlBuilder.ToString();

6.3 Create the Request

Next, you create the request, using a HttpRequestMessage:

var request = new HttpRequestMessage { }

The request must have the following properties:

  • RequestUri – the URI to which the request is sent. This is the InterviewUrl, created above.
  • Method – the method used to send the request. Creating an Interview requires the POST method.
  • Content – the content to be sent with the request. In this example, the content is the Template Package file.

Add the properties to the request, like this:

RequestUri = new Uri(InterviewUrl),
Method = HttpMethod.Post,
Content = GetAnswers()

For the content data to be added to the request, you must create a new method, GetAnswers, to retrieve the answer data used to assemble the document. You will create this method in the next step.

Finally, add the following content headers to the request:

request.Headers.TryAddWithoutValidation("x-hd-date", timestamp.ToString("yyyy-MM-ddTHH:mm:ssZ"));
request.Content.Headers.Remove("Content-Type");
request.Headers.TryAddWithoutValidation("Content-Type", "text/xml");
request.Headers.TryAddWithoutValidation("Authorization", hmac);
request.Headers.Add("Keep-Alive", "false");

You can now return the request message. The full method looks as follows:

private HttpRequestMessage CreateHttpRequestMessage(string hmac, string subscriberId, string packageId, DateTime timestamp, string billingRef, string format, string tempImageUrl, Dictionary<string, string> settings)
{
            var partialInterviewUrl = string.Format("https://cloud.hotdocs.ws/hdcs/interview/{0}/{1}?billingRef={2}&format={3}&tempimageurl={4}", subscriberId, packageId, billingRef, format, tempImageUrl);
            var completedInterviewUrlBuilder = new StringBuilder(partialInterviewUrl);
            foreach (var kv in settings)
            {
                completedInterviewUrlBuilder.AppendFormat("&{0}={1}", kv.Key, kv.Value ?? "");
            }
            var InterviewUrl = completedInterviewUrlBuilder.ToString();
            var request = new HttpRequestMessage
            {
                RequestUri = new Uri(InterviewUrl),
                Method = HttpMethod.Post,
                Content = GetAnswers()
            };
            // 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/xml");
            request.Headers.TryAddWithoutValidation("Authorization", hmac);
            request.Headers.Add("Keep-Alive", "false");
            return request;
}

7. Retrieve the File Content

If you wish to pre-populate the interview with answers, answer XML should be provided with the get interview request. You will do this with a new method, GetAnswers:

private 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>");
}

This method will retrieves HotDocs answer XML as StringContent. It can now be added to the request message. This step is optional. If you do not want to pre-populate the interview with answers, you should remove Content from the HttpRequestMessage.

8. Send the Request

Now that the file content has been added to the request message, the request can be sent to Cloud Services. In the GetInterviewResponse method, you will now send the request and retrieve the response. In Main, create a new HttpClient:

var client = new HttpClient();    

This is used to send the request. Next, to send the request, pass the request message to the HttpClient method, SendAsync:

var response = client.SendAsync(request);

Once the response is received, it can be returned:

return response.Result;

9. Completed GetInterviewResponse method

The full GetInterviewResponse method looks as follows:

public HttpResponseMessage GetInterviewResponse()
{
        // Cloud Services Subscription Details
        string subscriberId = "example-subscriber-id";
        string signingKey = "example-signing-key";
        // HMAC calculation data      
        var timestamp = DateTime.UtcNow;
        var packageId = "HelloWorld";
        var format = "JavaScript";
        var templateName = "";
        var sendPackage = false;
        var billingRef = "ExampleBillingRef";
        var tempImageUrl = "http://localhost/HDServerFiles/temp";            
        var settings = new Dictionary<string, string>
        {
                {"HotDocsJsUrl", "https://localhost/HDInterviewFiles/scripts/"},
                {"HotDocsCssUrl", "https://localhost/HDInterviewFiles/stylesheets/hdsuser.css"}, 
                {"InterviewDefUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewDefinition/"}, 
                {"SaveAnswersPageUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/SaveAnswers/"},                
                {"FormActionUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewFinish/"}
        };
        // Generate HMAC using Cloud Services signing key            
        string hmac = CalculateHMAC(signingKey, timestamp, subscriberId, packageId, templateName, sendPackage, billingRef, format, tempImageUrl, settings);            
        // Create assemble request            
        var request = CreateHttpRequestMessage(hmac, subscriberId, packageId, timestamp, billingRef, format, tempImageUrl, settings);
        //Send assemble request to Cloud Services
        var client = new HttpClient();
        var response = client.SendAsync(request);        
    
        return response.Result;
}

10. Using the Response

To display an interview, you need to extract several interview files from the response message. These are:

  • Interview HTML Fragment – HTML used when displaying the interview
  • Interview Definition File – file containing a definition of the interview content

In the next steps, you will create methods to get these files from the response message and use them to render the interview.

10.1 Get Multipart Stream from the Interview Response Message

In the response message, the interview files are contained in a multipart stream. To get the files from the multipart stream, you need to read the stream and return the contents as a collection of individual streams.

10.1.1 Create the GetIndividualStreamsFromMultipartStream method

First, create a new method to retrieve the individual streams from the multipart stream, GetIndividualStreamsFromMultipartStream:

private async Task<IEnumerable<HttpContent>> GetIndividualStreamsFromMultipartStream(HttpResponseMessage response)
{
}

In this method, individual interview file streams are asynchronously read from the multipart stream.

10.1.2 Read the Multipart Stream

In the GetIndividualStreamsFromMultipartStream method, add an IEnumerable collection to hold the individual interview file streams as they are read:

var individualInterviewFileStreams = Enumerable.Empty<HttpContent>();

Next, use a Task Factory to read all of the individual file streams from the multipart stream:

Task.Factory.StartNew(
    () => individualInterviewFileStreams = response.Content.ReadAsMultipartAsync().Result.Contents,
        CancellationToken.None,
        TaskCreationOptions.LongRunning,
        TaskScheduler.Default
).Wait();

The collection of file streams can now be returned. The full GetIndividualStreamsFromMultipartStream method looks as follows:

static async Task<IEnumerable<HttpContent>> GetIndividualStreamsFromMultipartStream(HttpResponseMessage response)
{
        var individualInterviewFileStreams = Enumerable.Empty<HttpContent>();
        Task.Factory.StartNew(
                () => individualInterviewFileStreams = response.Content.ReadAsMultipartAsync().Result.Contents,
                               CancellationToken.None,
                               TaskCreationOptions.LongRunning,
                               TaskScheduler.Default
        ).Wait();
        return individualInterviewFileStreams;
}

10.2 Get Interview Files from the Individual File Streams

Once the individual file streams are extracted from the multipart stream, their content can be read and used to generate the interview.

10.2.1 Create the GetInterviewFilesFromStream method

To read the content from the individual interview file streams, create a new method, GetInterviewFilesFromStream:

private async Task<Dictionary<string, Stream>> GetInterviewFilesFromStream(IEnumerable<HttpContent> individualInterviewFileStreams)
{
}

10.2.2 Read the Stream Content

First, add a Dictionary collection to hold the file name and content for each file:

Dictionary<string, Stream> interviewFiles = new Dictionary<string, string>();

Next, iterate through the individual file streams collection passed into the method:

foreach (var fileStream in individualInterviewFileStreams)
{
}

For each file stream, extract the file content and filename, and add them to the dictionary collection:

var fileContent = await fileStream.ReadAsStringAsync();
var filename = fileStream.Headers.ContentDisposition.FileName;
interviewFiles.Add(filename, fileContent);

Once each interview file  is added to the collection, the dictionary can be returned. The full GetInterviewFilesFromStream  method looks like this:

private async Task<Dictionary<string, Stream>> GetInterviewFilesFromStream(IEnumerable<HttpContent> individualInterviewFileStreams)
{
        Dictionary<string, Stream> interviewFiles = new Dictionary<string, string>();
        foreach (var fileStream in individualInterviewFileStreams)
        {
                var fileContent = await fileStream.ReadAsStreamAsync();
                var filename = fileStream.Headers.ContentDisposition.FileName;
              interviewFiles.Add(filename, fileContent);
        }
        return interviewFiles;
}

10.3 Save the Interview Files to Disk

Not all Interview Files retrieved from the response are passed immediately  to the interview. Callbacks from the interview will request interview  files while the interview is initializing. You host application must provide  the interview files when requested. In this example, you will save the  files to disk, for later retrieval.

10.3.1 Create the SaveInterviewFilesToTempDirectory method

First, create a new method, SaveInterviewFilesToTempDirectory:
private void SaveInterviewFilesToTempDirectory(Dictionary<string, Stream> interviewFiles)
{
}
This method takes the interview files collection returned from GetInterviewFilesFromStream  and saves the files to a temporary directory.

10.3.2 Write the Files to the Temporary Directory

Next, iterate through the interview files collection:
foreach (var file in interviewFiles)
{
        var filePath = String.Format(@"C:\temp\{0}", file.Key);
        File.WriteAllText(filePath, file.Value);
}

For each file, get the decoded file name and file extension:

var decodedFileName = HttpUtility.UrlDecode(file.Key);
var fileExtension = Path.GetExtension(file.Key);

Check the file extension, to see if the file is an image. If the file is an image, save it to the temp directory as an image file:

if (fileExtension == ".gif" || fileExtension == ".bmp" || fileExtension == ".jpg")
{                   
        var image = Image.FromStream(file.Value);
        image.Save(@"C:\temp\" + decodedFileName);                 
}

If the file is not an image, save it to the temp directory as a text file:

else
{
        var filePath = String.Format(@"C:\temp\{0}", decodedFileName);
        var textContent = new StreamReader(file.Value, Encoding.UTF8).ReadToEnd();
        File.WriteAllText(filePath, textContent);
}

10.3.3 Completed SaveInterviewFilesToTempDirectory method

The complete SaveInterviewFilesToTempDirectory method looks as follows:

private void SaveInterviewFilesToTempDirectory(Dictionary<string, Stream> interviewFiles)
{
        foreach (var file in interviewFiles)
        {                
                var decodedFileName = HttpUtility.UrlDecode(file.Key);
                var fileExtension = Path.GetExtension(file.Key);
                if (fileExtension == ".gif" || fileExtension == ".bmp" || fileExtension == ".jpg")
                {                   
                        var image = Image.FromStream(file.Value);
                        image.Save(@"C:\temp\" + decodedFileName);                 
                }
                else
                {
                        var filePath = String.Format(@"C:\temp\{0}", decodedFileName);
                        var textContent = new StreamReader(file.Value, Encoding.UTF8).ReadToEnd();
                        File.WriteAllText(filePath, textContent);
                }
        }
} 

11. Get the Interview Files and return the Interview HTML Fragment

You can now use all of the methods created above to get the interview  files, save the interview files to disk, and return the Interview HTML  Fragment required to display the interview. This requires the following  steps:
  • Get the Interview response from Cloud Services
  • Get Individual Files Streams from the Multipart stream
  • Get the interview files from the individual file streams
  • Save the interview files to the temporary directory
  • Return the Interview HTML Fragment
In this step, you will create a new method to perform these tasks.

11.1 Create the GetInterviewFragment method

To retrieve the interview fragment, create a new method, GetInterviewFragment:
public string GetInterviewFragment()
{
}
This is the public method that the HomeController will use to request  the Interview HTML fragment to pass to the View.

11.2 Perform the Steps to Retrieve the Interview Files

You will now use the methods created above to retrieve the files. First,  make the Get Interview Request to Cloud Services and get the response:

var interviewResponse = GetInterviewResponse();

Next, get the individual interview file streams from the response:

var individualInterviewFileStreams  = GetIndividualStreamsFromMultipartStream(interviewResponse);

Using the individual file streams, get the interview files:

var interviewFiles = GetInterviewFilesFromStream(individualInterviewFileStreams.Result);

Once the interview files are retrieved, save the files to the temporary  directory:

SaveInterviewFilesToTempDirectory(interviewFiles.Result);

Finally, retrieve and return the saved Interview HTML Fragment:

var interviewHtmlFragment  = File.ReadAllText(String.Format(@"C:\temp\fragment.txt"));

return interviewHtmlFragment;

11.3 The Complete GetInterviewFragment method

The completed GetInterviewFragment method looks like this:
public string GetInterviewFragment()
{            
        var interviewResponse = GetInterviewResponse();
        var individualInterviewFileStreams = GetIndividualStreamsFromMultipartStream(interviewResponse);
        
        var interviewFiles = GetInterviewFilesFromStream(individualInterviewFileStreams.Result);
        SaveInterviewFilesToTempDirectory(interviewFiles.Result);
            
        var interviewHtmlFragment = File.ReadAllText(String.Format(@"C:\temp\fragment.txt"));
        return interviewHtmlFragment;
}  
The Interview HTML Fragment can now be used in the Controller.

12. Using the Interview Files in the Controller

To render the interview, the Interview HTML Fragment generated by the  GetInterviewFragment method is passed to a View by the Controller. When  the interview initializes, it makes callbacks to the host application  to request additional interview files, including the Interview  Definition File and Image Files.  In this step, you will create controller methods to pass the Interview  HTML Fragment to a View and retrieve the Interview Definition File.

12.1 Pass the Interview HTML Fragment to a View

12.1.1 Create a View

If it does not already exist in your project, add a new Index.cshtml  view to the View folder:
  1. In the CloudServicesAPIExample3Interview project, navigate to Views > Home
  2. Right-click on the Home folder
  3. Select Add > View
  4. Enter Index.cshtml in the View Name field
  5. Click Add
The new Index.cshtml view is added to the folder.

12.1.2 Edit the View

Open the Index.cshtml file and  add the following line:

@Html.Raw(ViewBag.Message)

This line embeds the Interview HTML Fragment in the page.

12.1.3 Get the Interview HTML Fragment

To pass the Interview Fragment to the View, it must be retrieved using  the GetInterviewFragment method created above. To do this, open the HomeController.cs file and add the  following lines to the Index ActionResult  method:
var interviewRequest = new InterviewRequest();
var interviewFragment = interviewRequest.GetInterviewFragment();
In the View, you will use the ViewBag to embed the interview fragment  in the page. Assign the interview fragment to the ViewBag and return the  View:

ViewBag.Message = interviewFragment;

return View();  

12.2 Return the Interview Definition File during Interview Initialization

Before the interview can  be displayed, you need to retrieve and return the Interview Definition  file when it is requested during interview initialization.

When creating the interview  request, you specified a URL for the interviewDefUrl  Interview Setting. This setting provides the URL for a host application  endpoint from which the Interview Definition File is returned, in this  example, http://localhost/CloudServicesAPIExample3Interview/Home/InterviewDefinition/.  For the file to be returned, you must create a new InterviewDefinition  method in the HomeController.

12.1 Create the InterviewDefinition method

In the HomeController, create a new method, InterviewDefinition:
public string InterviewDefinition()
{
}
This method retrieves the contents of the saved Interview Definition  File and returns the contents as a string. Next, get the name of the requested Template from the QueryString:

var templateFileName = Request.QueryString["template"];

The file name for the Interview Definition File always takes the following  format:

{TemplateFileName}.js

For example, a HotDocs Template File named EmploymentAgreement.docx  will have an Interview Definition File named EmploymentAgreement.docx.js.  In this way, you can use the Template Name sent back from the interview  to retrieve the correct Interview Definition File. Using the Template File Name, create a full file path for the Interview  Definition File:

var templateJavaScriptFilePath  = String.Format(@"C:\temp\{0}.js", templateFileName);

Finally, retrieve and return  the contents of the Interview Definition File:

return System.IO.File.ReadAllText(templateJavaScriptFilePath);

The completed InterviewDefinition  method looks as follows:

public string InterviewDefinition()
{
        var templateFileName = Request.QueryString["template"];
        var templateJavaScriptFilePath = String.Format(@"C:\temp\{0}.js", templateFileName);
        return System.IO.File.ReadAllText(templateJavaScriptFilePath);
}

Once the Interview Definition  File is returned, the interview can completely render. The project is  now ready to test.

Testing

To test creating an interview using Cloud Services:
  1. Set the current project as the Startup Project (Right-click the CloudServicesAPIExample3Interview project in Visual Studio and select Startup Project from the drop-down menu)
  2. Press F5 to run the Project. The interview loads in the web browser.

Source Code (C#)

InterviewRequest.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;using System.Web;
using System.Drawing;

namespace CloudServicesAPIExample3Interview
{
    public class InterviewRequest
    {
        public string GetInterviewFragment()
        {            
            // Get Interview Response from Cloud Services
            var interviewResponse = GetInterviewResponse();
            //Retrieve the multipart file stream containing interview files
            var individualInterviewFileStreams = GetIndividualStreamsFromMultipartStream(interviewResponse);
            // Extract the interview files
            var interviewFiles = GetInterviewFilesFromStream(individualInterviewFileStreams.Result);
            // Save the Template File JavaScript to the temp folder
            SaveInterviewFilesToTempDirectory(interviewFiles.Result);
            
            // Retrieve the Interview HTML fragment
            var interviewHtmlFragment = File.ReadAllText(String.Format(@"C:\temp\fragment.txt"));

            return interviewHtmlFragment;
        }                   
        public HttpResponseMessage GetInterviewResponse()
        {
            // Cloud Services Subscription Details
            string subscriberId = "example-subscriber-id";
            string signingKey = "example-signing-key";
            // HMAC calculation data      
            var timestamp = DateTime.UtcNow;
            var packageId = "ed40775b-5e7d-4a51-b4d1-32bf9d6e9e29";
            var format = "JavaScript";
            var templateName = "";
            var sendPackage = false;
            var billingRef = "ExampleBillingRef";
            var tempImageUrl = "http://localhost/HDInterviewFiles/"; // IIS application must use the physical path to which interview files are saved, i.e. C:\temp\            
            var settings = new Dictionary<string, string>
            {
                {"HotDocsJsUrl", "https://localhost/HDInterviewFiles/scripts/"},
                {"HotDocsCssUrl", "https://localhost/HDInterviewFiles/stylesheets/hdsuser.css"}, 
                {"InterviewDefUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewDefinition/"}, 
                {"SaveAnswersPageUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/SaveAnswers/"},                
                {"FormActionUrl", "http://localhost/CloudServicesAPIExample3Interview/Home/InterviewFinish/"}
            };
            // Generate HMAC using Cloud Services signing key            
            string hmac = CalculateHMAC(signingKey, timestamp, subscriberId, packageId, templateName, sendPackage, billingRef, format, tempImageUrl, settings);            
            // Create assemble request            
            var request = CreateHttpRequestMessage(hmac, subscriberId, packageId, timestamp, billingRef, format, tempImageUrl, settings);
            //Send assemble request to Cloud Services
            var client = new HttpClient();
            var response = client.SendAsync(request);        
    
            return response.Result;
        }
        private async Task<IEnumerable<HttpContent>> GetIndividualStreamsFromMultipartStream(HttpResponseMessage response)
        {
            var individualInterviewFileStreams = Enumerable.Empty<HttpContent>();
            Task.Factory.StartNew(
                () => individualInterviewFileStreams = response.Content.ReadAsMultipartAsync().Result.Contents,
                CancellationToken.None,
                TaskCreationOptions.LongRunning,
                TaskScheduler.Default
            ).Wait();
            return individualInterviewFileStreams;
        }
        private async Task<Dictionary<string, Stream>> GetInterviewFilesFromStream(IEnumerable<HttpContent> individualInterviewFileStreams)
        {
            Dictionary<string, Stream> interviewFiles = new Dictionary<string, string>();
            foreach (var fileStream in individualInterviewFileStreams)
            {
                var fileContent = await fileStream.ReadAsStreamAsync();
                var filename = fileStream.Headers.ContentDisposition.FileName;
                interviewFiles.Add(filename, fileContent);
            }
            return interviewFiles;
        }
        private void SaveInterviewFilesToTempDirectory(Dictionary<string, Stream> interviewFiles)
        {
            foreach (var file in interviewFiles)
            {
                 var decodedFileName = HttpUtility.UrlDecode(file.Key);
                 var fileExtension = Path.GetExtension(file.Key);
                
                 if (fileExtension == ".gif" || fileExtension == ".bmp" || fileExtension == ".jpg")
                 {    
                      var image = Image.FromStream(file.Value);
                      image.Save(@"C:\temp\" + decodedFileName);                 
                 }
                 else
                 {
                      var filePath = String.Format(@"C:\temp\{0}", decodedFileName);
                      var textContent = new StreamReader(file.Value, Encoding.UTF8).ReadToEnd();
                      File.WriteAllText(filePath, textContent);
                 }
            }         }

        private HttpRequestMessage CreateHttpRequestMessage(string hmac, string subscriberId, string packageId, DateTime timestamp, string billingRef, string format, string tempImageUrl, Dictionary<string, string> settings)
        {
            var partialInterviewUrl = string.Format("https://cloud.hotdocs.ws/hdcs/interview/{0}/{1}?billingRef={2}&format={3}&tempimageurl={4}", subscriberId, packageId, billingRef, format, tempImageUrl);
            var completedInterviewUrlBuilder = new StringBuilder(partialInterviewUrl);
            foreach (var kv in settings)
            {
                completedInterviewUrlBuilder.AppendFormat("&{0}={1}", kv.Key, kv.Value ?? "");
            }
            var InterviewUrl = completedInterviewUrlBuilder.ToString();
            var request = new HttpRequestMessage
            {
                RequestUri = new Uri(InterviewUrl),
                Method = HttpMethod.Post,
                Content = GetAnswers()
            };
            // 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/xml");
            request.Headers.TryAddWithoutValidation("Authorization", hmac);
            request.Headers.Add("Keep-Alive", "false");
            return request;
        }
        private 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 >");
        }
        private string CalculateHMAC(string signingKey, params object[] paramList)
        {
            byte[] key = Encoding.UTF8.GetBytes(signingKey);
            string stringToSign = CanonicalizeParameters(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);
        }
        private string CanonicalizeParameters(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;
using System.Web.Mvc;
namespace CloudServicesAPIExample3Interview.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var interviewRequest = new InterviewRequest();
            var interviewFragment = interviewRequest.GetInterviewFragment();
            ViewBag.Message = interviewFragment;
            return View();
        }
        public string InterviewDefinition()
        {
            var templateFileName = Request.QueryString["template"];
            var templateJavaScriptFilePath = String.Format(@"C:\temp\{0}.js", templateFileName);
            return System.IO.File.ReadAllText(templateJavaScriptFilePath);            
        }
    }
}

Index.cshtml

@Html.Raw(ViewBag.Message)