<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Dac Thach Nguyen</title><link href="http://world.optimizely.com" /><updated>2019-02-20T04:05:42.0000000Z</updated><id>https://world.optimizely.com/blogs/dac-thach-nguyen/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Custom validation message for Forms</title><link href="https://world.optimizely.com/blogs/dac-thach-nguyen/dates/2019/2/custom-validation-message-for-forms/" /><id>&lt;p&gt;In Forms 4.23 we have released a feature that allows user can enter a custom validation message for each validator. Let say that you have some forms which used the same some validators (ex: RequiredValidator). When visitor enter something not correct, by default the form will display message stored in xml lang files. And there&#39;s no way to custom that message for different elements in different forms. But now it is easily done by editor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First go to edit the field and enter the message on textbox next to it:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/85ee6aa66c234050b5fa435ac6ba5637.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publish the element, form and go to view mode to see the result:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/c2db503640d343bead5161acf10e05a9.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That is, it&#39;s simple, right?&lt;/p&gt;</id><updated>2019-02-20T04:05:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>EPiServer Forms: How to render Forms in a dialog</title><link href="https://world.optimizely.com/blogs/dac-thach-nguyen/dates/2018/12/episerver-forms-how-to-render-forms-in-a-dialog/" /><id>&lt;p&gt;In some real scenarios, we might need render an EPiServer Forms within a dialog instead of a page. This case will make user focus on the Form. Today I have tried to make the Form work with jQuery UI dialog. Here a some point I summarized to make it works (assume that you are working with Alloy solution).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a page type to render an ContentArea with a Form&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[SiteContentType(GroupName = Global.GroupNames.News, GUID = &quot;E5CCD734-81F2-4242-82BA-8D888504112B&quot;)]
public class SubcriblePage: SitePageData
{
    [Display(GroupName = SystemTabNames.Content, Order = 320)]
    public virtual ContentArea MainContentArea { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create a page with the page type then dnd a Form into it, publish it (let say it url is: &quot;&lt;strong&gt;/en/subcrible/&lt;/strong&gt;&quot;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a zaror layout to render a page which display nothing but Forms (&lt;/strong&gt;_Empty.cshtml&lt;strong&gt;):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;@using System.Web.Optimization
@using EPiServer.Framework.Web.Mvc.Html
@model IPageViewModel&amp;lt;SitePageData&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;@Model.CurrentPage.LanguageBranch&quot;&amp;gt;
&amp;lt;head&amp;gt;
    @Styles.Render(&quot;~/bundles/css&quot;)
    @Scripts.Render(&quot;~/bundles/js&quot;)
    @Html.RequiredClientResources(&quot;Header&quot;)
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    @RenderBody()
    @Html.RequiredClientResources(&quot;Footer&quot;)
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Create a view to render for the page type (SubcriblePage)&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;@{ Layout = &quot;~/Views/Shared/Layouts/_Empty.cshtml&quot;; }
@using ILMTest
@model PageViewModel&amp;lt;SubcriblePage&amp;gt;

@Html.PropertyFor(x =&amp;gt; x.CurrentPage.MainContentArea, new { })
&amp;lt;script&amp;gt;
    if (typeof $$epiforms !== &#39;undefined&#39;) {
        $$epiforms(document).ready(function myfunction() {
            $$epiforms(&quot;.EPiServerForms&quot;).on(&quot;formsSubmitted&quot;, function (event, param1, param2) {
                window.parent.$(window.parent.document.body).trigger(&quot;submitted&quot;);
            });
        });
    }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This razor view will render an ContentArea with the Form. Add the script will notify parent window to know when the form already submitted (we will close the dialog then).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open the dialog with a Form (Using iframe which loaded the page alread created above)&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    var page = &quot;/en/subcrible/&quot;;

    var $dialog = $(&#39;&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&#39;)
        .html(&#39;&amp;lt;iframe style=&quot;border: 0px; overflow: hidden;&quot; src=&quot;&#39; + page + &#39;&quot; width=&quot;100%&quot; height=&quot;100%&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&#39;)
        .dialog({
            autoOpen: false,
            modal: true,
            height: 450,
            width: 500,
            title: &quot;Some title&quot;
        });
    $dialog.dialog(&#39;open&#39;);

    $(document).on(&quot;submitted&quot;, &quot;body&quot;, function () {
        setTimeout(function () {
            $dialog.dialog(&quot;close&quot;);
        }, 1000)
    });
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://imgur.com/a/E2UVHGD&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://i.imgur.com/9LgGO1k.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;</id><updated>2018-12-25T09:04:04.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Language Manager: Replace Content with Unpublished Version</title><link href="https://world.optimizely.com/blogs/dac-thach-nguyen/dates/2018/10/language-manager-replace-content-with-unpublished-version/" /><id>&lt;p&gt;When using Language Manager (LM) to duplicate content from other language. It always get content from Published or Common Draft version. There is a customer they want to get content from Ready To Publish version instead. To change the behavior we can intercept the&amp;nbsp;&lt;span&gt;LanguageBranchManager service. In&amp;nbsp;IConfigurableModule you add following line:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void ConfigureContainer(ServiceConfigurationContext context)
{
    context.Services.Intercept&amp;lt;ILanguageBranchManager&amp;gt;((locator, defaultManager) =&amp;gt; new MyLanguageBranchManager(defaultManager));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;And implment the&amp;nbsp;MyLanguageBranchManager, most of methods we should forward to original service to process. We just modify the [CopyDataFromMasterBranch] method for copying content from Ready To Publish version instead of Published/CommonDraft one. Below is the code for doing this:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public bool CopyDataFromMasterBranch(ContentReference contentReference, string fromLanguageID, string toLanguageID, Func&amp;lt;object, object&amp;gt; transformOnCopyingValue, out ContentReference createdContentLink, bool autoPublish = false)
{
    fromLanguageID = fromLanguageID.Trim();
    toLanguageID = toLanguageID.Trim();

    createdContentLink = null;

    // get ReadyToPublish version here to process copying, fallback to Published or CommonDraft version
    var masterContent = GetReadyForPublishVersion(contentReference, fromLanguageID) ?? GetPublishedOrCommonDraftVersion(contentReference, fromLanguageID);
    var destContent = GetPublishedOrCommonDraftVersion(contentReference, toLanguageID);

    if (masterContent == null)
    {
        throw new ContentNotFoundException(contentReference);
    }

    if (destContent == null)
    {
        CreateLanguageBranch(contentReference, toLanguageID, out createdContentLink);
    }
    var createdDestContent = contentRepository.Service.Get&amp;lt;IContent&amp;gt;(contentReference.ToReferenceWithoutVersion(), new LanguageSelector(toLanguageID));
    createdDestContent = (createdDestContent as IReadOnly).CreateWritableClone() as IContent;

    #region process for Name, PageURLSegment property of both page and block
    if (transformOnCopyingValue == null)
    {
        createdDestContent.Name = masterContent.Name;
    }
    else
    {
        createdDestContent.Name = transformOnCopyingValue(masterContent.Name) as string;
        if (masterContent.Property[&quot;PageURLSegment&quot;] != null)
        {
            string url = transformOnCopyingValue(masterContent.Property[&quot;PageURLSegment&quot;].ToWebString().Replace(&#39;-&#39;, &#39; &#39;)) as string;   /* the result from Bing */
            url = Regex.Replace(url, @&quot;\s+&quot;, &quot; &quot;, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant).Trim(); // convert multiple spaces into one space
            url = Regex.Replace(url, @&quot;\s&quot;, &quot;-&quot;, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Replace spaces by dashes
            url = Regex.Replace(url, @&quot;[^a-z0-9~_\-\.]&quot;, matchEvaluatorRandomReplace, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); // Remove all non valid chars
            createdDestContent.Property[&quot;PageURLSegment&quot;].Value = url;
        }
    }
    #endregion

    #region process all LanguageSpecific, non-meta, editable properties

    foreach (PropertyData destProp in createdDestContent.Property.Where(pd =&amp;gt;
        (pd.IsLanguageSpecific &amp;amp;&amp;amp; !pd.IsMetaData &amp;amp;&amp;amp; !pd.IsReadOnly) // find all non-metadata properties, which is editable, language specific
        ))
    {
        var nestedContentData = destProp.Value as IContentData;     // block
        if (null != nestedContentData) // if is block, ...
        {
            CopyDataForNestedContentRecursive(masterContent.Property[destProp.Name] as IContentData, nestedContentData, transformOnCopyingValue);
        }
        else
        {
            // some data type and its property type (e.g. CategoryList &amp;amp; PropertyCategory) implements IModifiedTrackable and then a call to IsModified 
            // (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
            var srcPropValue = masterContent.Property[destProp.Name].Value;
            destProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
            if (transformOnCopyingValue != null &amp;amp;&amp;amp; destProp.Value != null)
            {
                var translatedText = transformOnCopyingValue(destProp);
                TryToAssignStringToProperty(translatedText, destProp);
            }
        }
    }   // end foreach prop in page level

    #endregion

    // skip validation, because translate might be failed sometime.
    var saveFlag = SaveAction.Save | SaveAction.SkipValidation;
    saveFlag = (createdDestContent as IVersionable).Status == VersionStatus.Published ?
        saveFlag | SaveAction.ForceNewVersion : saveFlag | SaveAction.ForceCurrentVersion;
    if (autoPublish)
    {
        saveFlag = saveFlag | SaveAction.Publish;
    }

    createdContentLink = contentRepository.Service.Save(createdDestContent, saveFlag, AccessLevel.NoAccess);
    contentVersionRepository.Service.SetCommonDraft(createdContentLink);

    return true;

    // return _defaultLanguageBranchManager.CopyDataFromMasterBranch(contentReference, fromLanguageID, toLanguageID, transformOnCopyingValue, out createdContentLink, autoPublish);
}

private IContent GetReadyForPublishVersion(ContentReference contentLink, string languageID)
{
    var contentVersionRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentVersionRepository&amp;gt;();
    var versions = contentVersionRepository.List(contentLink, languageID);
    var readyToPublishVersion = versions.FirstOrDefault(v =&amp;gt; v.Status == VersionStatus.CheckedIn);

    var contentRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;();
    if (readyToPublishVersion != null)
    {
        return contentRepository.Get&amp;lt;IContent&amp;gt;(readyToPublishVersion.ContentLink);
    }

    return null;
}

/// &amp;lt;summary&amp;gt;
/// Copies the data for nested content recursively.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;sourceContentData&quot;&amp;gt;The source content data.&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;targetContentData&quot;&amp;gt;The target content data.&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;transformOnCopyingValue&quot;&amp;gt;perform a transformation on copying property&#39;s value&amp;lt;/param&amp;gt;
private void CopyDataForNestedContentRecursive(IContentData sourceContentData, IContentData targetContentData, Func&amp;lt;object, object&amp;gt; transformOnCopyingValue)
{
    using (IEnumerator&amp;lt;PropertyData&amp;gt; propertyDataEnumerator = targetContentData.Property
        .Where(pd =&amp;gt; pd.IsLanguageSpecific &amp;amp;&amp;amp; !pd.IsMetaData &amp;amp;&amp;amp; !pd.IsReadOnly)
        .GetEnumerator())
    {
        while (propertyDataEnumerator.MoveNext())
        {
            var targetProp = propertyDataEnumerator.Current;
            if (targetProp is IContentData)
            {
                CopyDataForNestedContentRecursive(sourceContentData.Property[targetProp.Name] as IContentData, targetProp as IContentData, transformOnCopyingValue);
            }
            else
            {
                // some data type and its property type (e.g. CategoryList &amp;amp; PropertyCategory) implements IModifiedTrackable and then a call to IsModified 
                // (leading to IReadOnly.ThrowIfReadOnly) from ContentRepository.Save below, so that must use WritableClone value
                var srcPropValue = sourceContentData.Property[targetProp.Name].Value;
                targetProp.Value = srcPropValue is IReadOnly ? ((IReadOnly)srcPropValue).CreateWritableClone() : srcPropValue;
                if (transformOnCopyingValue != null)
                {
                    var translatedText = transformOnCopyingValue(targetProp);
                    TryToAssignStringToProperty(translatedText, targetProp);
                }
            }
        }
    }
}

/// &amp;lt;summary&amp;gt;
/// assign &amp;lt;paramref name=&quot;obj&quot;/&amp;gt; to &amp;lt;paramref name=&quot;prop&quot;/&amp;gt; might lead to exception because &amp;lt;paramref name=&quot;prop&quot;/&amp;gt; cannot accept string &amp;gt;255.
/// We try to shorten it before assigning again.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;obj&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;param name=&quot;prop&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
private void TryToAssignStringToProperty(object obj, PropertyData prop)
{
    try
    {
        prop.Value = obj;
    }
    catch (EPiServerException ex)
    {
        if (ex.Message.Contains(&quot;exceeded&quot;))    // exceeded 255 characters
        {
            prop.Value = ((string)obj).Substring(0, 255);
        }
        else
        {
            throw;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have similar requirement for your site. I hope this will help you a litle bit.&lt;/p&gt;</id><updated>2018-10-30T08:07:15.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Language Manager: Receive email notification when import translation package success</title><link href="https://world.optimizely.com/blogs/dac-thach-nguyen/dates/2018/7/language-manager-receive-email-notification-when-import-translation-package-success/" /><id>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;When import a translation package using Language add-on, the project owner will be notified after the package is proccessed. The notification will be display on the bell which is on top of edit view. That required you to login into the system to know.&amp;nbsp;The add-on allowed you to write your own notification provider which send notification in other channels.&lt;/p&gt;
&lt;p&gt;In order to write custom provider you need to implement&amp;nbsp;INotificationProvider which has method and property as below:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DisplayName:&amp;nbsp;&lt;/strong&gt;Name of the provider, example &quot;Email Notification&quot;&lt;br /&gt;&lt;strong&gt;SetNotification:&amp;nbsp;&lt;/strong&gt;Method will be called after the import proccess has been finished.&lt;/p&gt;
&lt;p&gt;After implement that interface, your notification provider will be display in config page of the add-on. See the image:&lt;img src=&quot;/link/39259a89e41f4f4999acbea2d8e09813.aspx&quot; alt=&quot;Image language.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here is simple on I have implement, after config it in the admin page. You should receive email whenever a translation project imported.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class EmailNotification : INotificationProvider
{
    public string DisplayName =&amp;gt; &quot;Email Notification&quot;;

    public void SetNotification(string userName, string message)
    {

        var mail = new MailMessage();
        mail.Subject = &quot;Project has been translated successfully&quot;;
        mail.Body = message;

        mail.From = new MailAddress(&quot;&amp;lt;your email&amp;gt;@gmail.com&quot;);
        mail.To.Add(new MailAddress(&quot;&amp;lt;your email&amp;gt;@gmail.com&quot;));

        var smtpClient = new SmtpClient();
        smtpClient.Send(mail);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</id><updated>2018-07-31T06:21:47.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>