Blog posts by Quan Tran2019-03-04T02:17:37.0000000Z/blogs/quan-tran/Optimizely WorldEPiServer.Forms show loading icon/blogs/quan-tran/dates/2019/3/episerver-forms-show-loading-icon/2019-03-04T02:17:37.0000000Z<p>When submission process takes a long time to complete, a loading icon should be shown just to let users know something is <span>happening. It is easily done with EPiServer.Forms</span></p>
<ol>
<li><span>Create </span>FormStyle.css<span> file to write cutom style for form and put it under </span><em>~/ClientResources/Styles</em><span> folder. </span></li>
</ol>
<pre class="language-css"><code>.modal {
display: none;
position: fixed;
z-index: 9999;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: rgba( 255, 255, 255, .8 ) url('http://i.stack.imgur.com/FhHRx.gif') 50% 50% no-repeat;
margin: 0;
}
.loading .modal {
overflow: hidden;
}
.loading .modal {
display: block;
}</code></pre>
<p><span>2. Create a file name </span><em>FormScript.js</em><span> and put it under </span><em>~/ClientResources/Scripts</em><span> folder. We will hook into Form's events to show/hide loading icon</span></p>
<pre class="language-javascript"><code>if (typeof $$epiforms !== 'undefined') {
var $body = $("body");
var $loadingDiv = $('<div class="modal"></div>');
$body.append($loadingDiv);
epi.EPiServer.Forms.AsyncSubmit = true;
$$epiforms(document).ready(function myfunction() {
// listen to event when form is about submitting
$$epiforms(".EPiServerForms").on("formsStartSubmitting", function (data) {
//var $formContainer = $('#' + data.workingFormInfo.Id);
//$formContainer.addClass("loading");
$body.addClass('loading');
});
// listen to event when form is successfully submitted
$$epiforms(".EPiServerForms").on("formsSubmitted", function (data) {
$body.removeClass('loading');
});
// formsSubmittedError
$$epiforms(".EPiServerForms").on("formsSubmittedError", function (data) {
$body.removeClass('loading');
});
});
}</code></pre>
<p><span>3. Register external client resources that we just created above to EPiServer.Forms.</span></p>
<pre class="language-csharp"><code>using System;
using System.Collections.Generic;
using EPiServer.Forms.Implementation;
using EPiServer.ServiceLocation;
/// <summary>
/// Register client resources to EPiServer.Forms
/// </summary>
[ServiceConfiguration(ServiceType = typeof(IViewModeExternalResources))]
public class ViewModeExternalResources : IViewModeExternalResources
{
public virtual IEnumerable<Tuple<string, string>> Resources
{
get
{
var arrRes = new List<Tuple<string, string>>();
arrRes.Add(new Tuple<string, string>("script", "/ClientResources/Scripts/FormScript.js"));
arrRes.Add(new Tuple<string, string>("css", "/ClientResources/Styles/FormStyle.css"));
return arrRes;
}
}
}</code></pre>
<p><span>When form is submitting, the page will look like this</span></p>
<p><span><img src="/link/7914611f434448fab277baac886e5475.aspx" /></span></p>
<p><span><strong>Note:</strong> If submission process does not take long we might not see the loading icon. In this case, debug on dev tools to see the result.</span></p>EPiServer.Forms create custom Action for Field Dependency /blogs/quan-tran/dates/2019/1/episerver-forms-create-custom-action-for-field-dependency-/2019-02-20T08:51:56.0000000Z<p>Feature dependency for fields is available from version 4.15 which lets you configure dependencies among fields in a form. You can create rules for field elements on a new Dependencies tab in the element properties. There are two buit-in actions Show and Hide by default.</p>
<p>In this post, I'm going to implement a simple custom action that can make the fields change its background color based on value of other field(s).</p>
<ol>
<li>Create class name <strong>RedBackgroundColorAction</strong> inherit from <strong>IDependencyAction</strong></li>
</ol>
<pre class="language-csharp"><code>using EPiServer.Forms.Core.Internal.Dependency;
using EPiServer.ServiceLocation;
namespace CustomFieldDependency
{
/// <summary>
/// New Action for form field dependency. Set the background color to form element.
/// </summary>
[ServiceConfiguration(typeof(IDependencyAction))]
public class RedBackgroundColorAction : IDependencyAction
{
/// <summary>
/// Display name of the action, show to Editor
/// </summary>
public string DisplayName => "In red background";
/// <summary>
/// Order in which the action will be listed in the dropdown for Editor
/// </summary>
public int Order => 3;
/// <summary>
/// Name of the action. This should be unique among others
/// </summary>
public string Name => GetType().FullName;
/// <summary>
/// Name of the method at clientside (Javascript), which will be called when dependency state changes
/// </summary>
public string ClientsideAction => "RedBackgroundColorAction";
}
}</code></pre>
<p>Let's rebuild your project, create a form with two fields: Text and Textarea. On Dependencies tab of Textarea element, the new action is listed. Select the newly created <em>In Red Background</em> action. The background color of Textarea element will be changed to red if the value of Text element contains <em>episerver</em></p>
<p><em><img src="/link/e1da4ac0798b4ed889b96933a7142321.aspx" /></em></p>
<p>2. We need to implement the action <strong>RedBackgroundColorAction</strong> on clide side. Create a file name <em>FormFieldDependency.js</em> and put it under <em>~/ClientResources/Scripts</em> folder</p>
<pre class="language-javascript"><code>(function ($) {
$.extend(true, epi.EPiServer.Forms.Dependency.Actions, {
//extending Actions for Form Field Dependency
RedBackgroundColorAction: function ( /*epi.EPiServer.Forms.Dependency.DependantController*/ controller) {
var elementName = controller.dependantInfo.fieldName;
var $wrapperElement = $('[data-f-element-name="' + elementName + '"]', controller.workingFormInfo.$workingForm);
var $inputElement = $('[data-f-datainput]', $wrapperElement); // or simply controller.$domElement
var dependencyInfo = getDependencyInfo(controller.workingFormInfo, controller.dependantInfo.fieldName); // or simply controller.dependantInfo
if (!dependencyInfo) {
return;
}
if (controller.isSatisfied) {
$inputElement.addClass('bg-red');
} else {
$inputElement.removeClass('bg-red');
}
}
});
// get dependency infor of a field
function getDependencyInfo(workingFormInfo, fieldName) {
var dependencies = workingFormInfo.DependenciesInfo;
if (!dependencies || dependencies.length === 0) {
return null;
}
for (var i = 0; i < dependencies.length; i++) {
if (dependencies[i].fieldName === fieldName) {
return dependencies[i];
}
}
return null;
}
})($$epiforms || $);</code></pre>
<p>3. Create <em>FormFieldDependency.css</em> file to write cutom style for elements and put it under <em>~/ClientResources/Styles</em> folder</p>
<pre class="language-css"><code>.bg-red {
background-color:red !important;
}</code></pre>
<p>4. Register external client resources to EPiServer.Forms. </p>
<pre class="language-csharp"><code>using System;
using System.Collections.Generic;
using EPiServer.Forms.Implementation;
using EPiServer.ServiceLocation;
namespace CustomFieldDependency
{
/// <summary>
/// Register client resources for EPiServer.Forms
/// </summary>
[ServiceConfiguration(ServiceType = typeof(IViewModeExternalResources))]
public class ViewModeExternalResources : IViewModeExternalResources
{
public virtual IEnumerable<Tuple<string, string>> Resources
{
get
{
var arrRes = new List<Tuple<string, string>>();
arrRes.Add(new Tuple<string, string>("script", "/ClientResources/Scripts/FormFieldDependency.js"));
arrRes.Add(new Tuple<string, string>("css", "/ClientResources/Styles/FormFieldDependency.css"));
return arrRes;
}
}
}
}</code></pre>
<p>Let's view the form in Viewmode, enter <em>episerver</em> in Text element and see the result.</p>
<p><img src="/link/4cf5bac336c04186b38b86f4fc74261d.aspx" /></p>
<p>Hope this help.</p>EPiServer.Forms storing upload file in HttpSession/blogs/quan-tran/dates/2019/1/episerver-forms-storing-upload-file-in-httpsession/2019-01-14T05:28:13.0000000Z<p>Few months ago I got a Forms support case from a customer. They had a concern for security perspective as well as GDPR perspective. The question is "Is<span> it possible to avoid saving files that are uploaded using the fileupload element and attach them to the e-mail instead of uploading to the the medie library and linking to them in the e-mail ?</span>" <span>The customer did not want to upload the files to the media library because malicious files could find their way in, and also because the file that is uploaded is very important so it needs to be attached to the e-mail as a file attachment.</span></p>
<p>Short answer is: Yes it's totally possible.</p>
<p>In order to avoid uploading files to media library and send attachment files instead of sending file links we have to override a couple of classes and methods.</p>
<ul>
<li>The idea is that, we will save the uploaded files in<span> </span><strong>HttpSession </strong>and later on when sending email in actor, we will get it back and attach them to the e-mail then remove it from <strong>HttpSession</strong>. But by doing so, we'll encounter some downsides regarding to Performance issue . Because by default,<span> </span><strong>SendEmailAfterSubmissionActor </strong>runs asynchronously after form submission therefore the<span> </span><strong>HttpContext.Current </strong>will be lost and we can not retrieve file from HttpSession. Thus,<span> </span>SendEmailAfterSubmissionActor<span> </span>must be run synchronously (will be overridden) to get<span> </span>HttpContext.Current<span> </span>. Storing files in HttpSession will cause performance penalties if thousands of users uploading large files at the same time .Furthermore, if file size is too large so it will not be able to attach to Email.</li>
</ul>
<ul>
<li>The approach above it works but not very well. Fortunately, we can implement in another way. We can upload file to third party (like GoogleDriver for example) and get it back then attach to Email.</li>
</ul>
<p><span>For the sake of simplicity, I'm going to implement the first approach. The steps are as below:</span></p>
<p><span>1. Create a class to override <strong>InsertPostedFilesToSubmittedData </strong>method in <strong>DataSubmissionService</strong>. The first argument <strong>elementFiles </strong>contains uploaded file in <strong>HttpPostedFileBase</strong>. From there, we'll save file to HttpSession with a key. That key will be saved into HttpContext and used later on when sending mail.</span></p>
<pre class="language-csharp"><code>/// <summary>
/// Model for storing upload file in session
/// </summary>
public class SessionStoreFile
{
public string FileName { get; set; }
public string Extension { get; set; }
public byte[] Data { get; set; }
}</code></pre>
<pre class="language-csharp"><code>/// <summary>
/// Helper class for handling file
/// </summary>
public class FileHelper
{
/// <summary>
/// Get binary data from <see cref="HttpPostedFileBase"/>
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public static byte[] GetBlobFromPostedFile(HttpPostedFileBase file)
{
byte[] blob;
using (BinaryReader reader = new BinaryReader(file.InputStream))
{
blob = reader.ReadBytes((Int32)file.InputStream.Length);
}
return blob;
}
}</code></pre>
<pre class="language-csharp"><code>public class CustomFormsDataSubmissionService : DataSubmissionService
{
protected override void InsertPostedFilesToSubmittedData(IEnumerable<Tuple<string, ContentReference, HttpPostedFileBase>> elementFiles, HttpContextBase httpContext, ref Dictionary<string, object> submittedData)
{
// IDEA : Store file in HttpSession and get it back when sending mail.AFter sending mail, remove it from session
// If you don't want to store in Session, you can upload it to third party like GoogleDrive
// if there's no upload file, do nothing
if (elementFiles == null || elementFiles.Count() == 0)
{
return;
}
// using DateTime.Now.Ticks as ID of each Post
var postId = DateTime.Now.Ticks;
var sessionStoreFiles = new List<SessionStoreFile>();
foreach (var item in elementFiles)
{
var postedFileBase = item.Item3;
if (postedFileBase == null)
continue;
sessionStoreFiles.Add(new SessionStoreFile
{
FileName = postedFileBase.FileName,
Extension = Path.GetExtension(postedFileBase.FileName),
Data = FileHelper.GetBlobFromPostedFile(postedFileBase)
});
}
if (sessionStoreFiles.Count == 0)
{
return;
}
var fileStoreKey = string.Format("_EpiFormUploadFile_{0}", postId);
// save file data to Session
var session = System.Web.HttpContext.Current.Session;
session[fileStoreKey] = sessionStoreFiles;
// save the file store key into HttpContext. This will be used in email actor later on.
httpContext.Items.Add("__EpiFormUploadFile_Session_StoreKey", fileStoreKey);
}
}</code></pre>
<p><span>2. Create a class name <strong>SendEmailAfterSubmissionActor </strong>to override <strong>Run </strong>method of <strong>EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor</strong>. Note that for some reasons, the new overridden class name must be exact SendEmailAfterSubmissionActor. Here, we will get uploaded files from HttpSession and attach it in the email then remove them from HttpSession.</span></p>
<pre class="language-csharp"><code>using EPiServer.Forms.Implementation.Actors;
using System;
using System.Linq;
using System.Web;
using EPiServer.Logging;
using EPiServer.Forms.Core.Internal;
using EPiServer.ServiceLocation;
using EPiServer.Forms.Helpers.Internal;
using System.Text.RegularExpressions;
using System.Net.Mail;
using System.Collections.Generic;
using System.IO;
using FormsFileSession.Customization;
/// <summary>
/// Custom actor for sending email.
/// </summary>
public class SendEmailAfterSubmissionActor : EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor
{
private readonly Injected<PlaceHolderService> _placeHolderService;
private bool _sendMessageInHTMLFormat = false;
private static SmtpClient _smtpClient = new SmtpClient();
/// <summary>
/// By default, this Actor run asynchronously after forms submission
/// In order to get file data from Session, we have to set this to FALSE so it will run synchronously right after form submission
/// NOTE: This will increase submission time
/// </summary>
public override bool IsSyncedWithSubmissionProcess { get { return true; } }
public SendEmailAfterSubmissionActor()
{
_sendMessageInHTMLFormat = _formConfig.Service.SendMessageInHTMLFormat;
}
public override object Run(object input)
{
var emailConfigurationCollection = Model as IEnumerable<EmailTemplateActorModel>;
if (emailConfigurationCollection == null || emailConfigurationCollection.Count() < 1)
{
_logger.Debug("There is no emailConfigurationCollection for this actor to work");
return null;
}
_logger.Information("Start sending email with {0} configuration entries in emailConfigurationCollection", emailConfigurationCollection.Count());
foreach (var emailConfiguration in emailConfigurationCollection)
{
SendMessage(emailConfiguration);
}
return null;
}
private void SendMessage(EmailTemplateActorModel emailConfig)
{
var toEmails = emailConfig.ToEmails;
if (string.IsNullOrEmpty(toEmails))
{
_logger.Debug("There is no ToEmails to send. Skip.");
return;
}
// replace placeholders with their value
var bodyPlaceHolders = GetBodyPlaceHolders(null);
var emailAddressToSend = _placeHolderService.Service.Replace(toEmails, bodyPlaceHolders, false);
// split To, get multiple email addresses and send email to each mail address later
var toEmailCollection = emailAddressToSend.SplitBySeparators(new[] { ",", ";", Environment.NewLine });
if (toEmailCollection == null || toEmailCollection.Count() == 0)
{
_logger.Debug("There is no ToEmails to send. Skip.");
return;
}
try
{
var subjectPlaceHolders = GetSubjectPlaceHolders(null);
var subject = _placeHolderService.Service.Replace(emailConfig.Subject, subjectPlaceHolders, false);
//replace line breaks by spaces in subject
var regexLineBreak = new Regex("(\r\n|\r|\n)");
subject = regexLineBreak.Replace(subject, " ");
// because the subject cannot display as HTML, we need to decode it
subject = HttpUtility.HtmlDecode(subject);
var bodyHtmlString = emailConfig.Body == null ? string.Empty : _formBusinessService.Service.ToHtmlStringWithFriendlyUrls(emailConfig.Body);
// body is inputted via tinyEMC will be saved as a HTML-encoded string, we should decode it before replacing with placeholders
var decodedBody = HttpUtility.HtmlDecode(bodyHtmlString);
var body = _placeHolderService.Service.Replace(decodedBody, bodyPlaceHolders, false);
var message = new MailMessage();
message.Subject = subject;
message.Body = RewriteUrls(body);
message.IsBodyHtml = _sendMessageInHTMLFormat;
#region ATTACHMENT_FILES
////////////////////////// ATTACHMENT ///////////////////////////
// get session store key of upload file
var httpContext = new System.Web.HttpContextWrapper(System.Web.HttpContext.Current) as HttpContextBase;
if (httpContext.Items.Contains("__EpiFormUploadFile_Session_StoreKey"))
{
var fileStoreKey = (string)httpContext.Items["__EpiFormUploadFile_Session_StoreKey"];
var httpSession = System.Web.HttpContext.Current.Session; // this.HttpRequestContext.RequestContext.HttpContext.Session;
if (httpSession != null)
{
var files = (List<SessionStoreFile>)httpSession[fileStoreKey];
if (files != null && files.Count > 0)
{
foreach (var file in files)
{
message.Attachments.Add(new Attachment(new MemoryStream(file.Data), file.FileName));
}
}
// remove file in session
httpSession[fileStoreKey] = null;
httpSession.Remove(fileStoreKey);
}
}
/////////////////////////// END ATTACHMENT /////////////////////////
#endregion
if (!string.IsNullOrEmpty(emailConfig.FromEmail))
{
var emailAddressFrom = _placeHolderService.Service.Replace(emailConfig.FromEmail, subjectPlaceHolders, false);
message.From = new MailAddress(emailAddressFrom);
}
foreach (var emailAddress in toEmailCollection)
{
try
{
// email from the EndUser cannot be trust, wrong email format can not be added.
message.To.Add(new MailAddress(emailAddress));
}
catch (Exception ex)
{
_logger.Debug(string.Format(@"{0} is not valid email addresses for email.To", emailAddress), ex);
}
}
_smtpClient.Send(message);
}
catch (Exception ex)
{
_logger.Error("Failed to send email", ex);
}
}
}</code></pre>
<p>3. We have to disable the built-in SendEmailAfterSubmissionActor actor to prevent sending email twice.</p>
<pre class="language-csharp"><code>using EPiServer.Forms.Core.PostSubmissionActor;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using EPiServer.Forms.Core.Models;
using EPiServer.Forms.Implementation.Elements;
/// <summary>
/// Provide submission data for Actors, instantiate and execute actors
/// </summary>
public class CustomActorsExecutingService : ActorsExecutingService
{
public override IEnumerable<IPostSubmissionActor> GetFormSubmissionActors(Submission submission, FormContainerBlock formContainer, FormIdentity formIden, HttpRequestBase request, HttpResponseBase response, bool isFormFinalizedSubmission)
{
var submissionActors = base.GetFormSubmissionActors(submission, formContainer, formIden, request, response, isFormFinalizedSubmission);
if (submissionActors == null || submissionActors.Count() == 0)
{
return Enumerable.Empty<IPostSubmissionActor>();
}
var filteredActors = submissionActors.ToList();
// remove the system's email-actor
filteredActors.RemoveAll(act => act.Name == typeof(EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor).FullName);
return filteredActors;
}
}
</code></pre>
<p>4. And don't forget to register dependencies</p>
<pre class="language-csharp"><code>public void ConfigureContainer(ServiceConfigurationContext context)
{
//Implementations for custom interfaces can be registered here.
context.ConfigurationComplete += (o, e) =>
{
context.Services.
AddTransient<EPiServer.Forms.Core.Internal.DataSubmissionService, CustomFormsDataSubmissionService>()
.AddTransient<EPiServer.Forms.Implementation.Actors.SendEmailAfterSubmissionActor, SendEmailAfterSubmissionActor>()
.AddTransient<ActorsExecutingService, CustomActorsExecutingService>();
};
}</code></pre>
<p>If you have any questions or better approach, please drop me a comment :)</p>Impact of Change Approvals on other addons along with its limitations/blogs/quan-tran/dates/2018/3/episerver-changeaprovals-first-realeas/2018-03-20T10:22:16.0000000Z<p>After months of development, ChangeApproval <strong>1.0.1</strong> was released yesterday, although it's still being actively developed. This article explains the impact of Change Approval on some addons as well as its limitations.</p>
<h2><strong>What are Change Approvals (CHA) ?</strong></h2>
<p>Those who are using Content Approvals will quickly become familiar with Change Approvals. <span>CHA </span>is similar to Content Approvals but CHA is for <em>settings</em> changes, while Content Approvals is for <em>content</em> changes.</p>
<p>With the features of CHA, all changes regarding access rights, language, expiration data and archiving, and hierarchy structure (moving pages/blocks to a new location in the hierarchy) must be reviewed and approved before being applied for the IT system. The review process is the same as that of Content Approval. CHA also uses the same sequence definition with Content Approvals. Therefore, users don't have to manage two definitions for two type of changes, which will reduce the complexity. </p>
<p>This article introduces CHA. For more details, visit CHA's <a href="http://webhelp.episerver.com/latest/en/cms-edit/change-approvals.htm">User Guide.</a></p>
<h3>Limitation</h3>
<ul>
<li>In the first release, <strong>Change Approvals only supports CMS 10.x</strong>. The version for CMS 11 will be available soon.</li>
<li>Only supports user interface notification (through the bell). </li>
<li>It’s unable to list all items waiting to be reviewed or items that were already reviewed in the task editor.</li>
<li>The notification icon for Change Approvals is not supported yet, so it might take a while for users to recognize the CHA notification type.</li>
</ul>
<h3><span>Impact on other addons</span></h3>
<h4><span>Language Manager (LM)</span></h4>
<p>LM has some impact on ChangeApproval, since an editor wants to add/disable a language of a page using LM.</p>
<ol>
<li>1. If the page does not inherit settings from parent</li>
</ol>
<ul>
<li>The page has no change approval associated with it. After the editor adds/disables a language, changes are captured for the review process and the flow works well as described above. Admins cannot change languages using the language dialog.</li>
<li>If an admin has changed languages of the page before using language dialog before, the editor can still use LM to add/disable languages, and changes can still be captured for the review process. However, change details now follow setting made by LM, not by language dialog.</li>
</ul>
<ol>
<li>2. if the page inherits settings from its parent</li>
</ol>
<ul>
<li>For version < 3.0.4, LM cannot work as expected for this case. It is recommended that two addons should not be used in parallel for this case.</li>
<li>For 3.0.4, LM works well and it should the same as (1).</li>
</ul>
<p>Optimization Block - OP</p>
<ul>
<li>If the site installing OP is set up under <em>IIS \ Virtual Directory</em>, ChangeApproval cannot be installed if the site is using an OP version lower than 2.3.2. Version 2.3.2 was published for compatability with CHA. Mostly, nothing special has been changed for this version. Otherwise, the site and OP should work well with CHA.</li>
</ul>
<p>More features will be added to the next release of ChangeApprovals.</p>