How to change the delay between subsequent saves in EPiServer TinyMCE

By default we would save every two seconds (of course only if the content was changed in any way, the save is throttled for 2 seconds by default). 

Unfortunately it is not possible to adjust that value using the fluent server side api.

Hopefully, we can easily change the setting by inheriting the default xhtml descriptor and providing our own value.

[EditorDescriptorRegistration(TargetType = typeof(XhtmlString), 
                              EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class CustomXhtmlStringEditorDescriptor : XhtmlStringEditorDescriptor
{
	public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
	{
		base.ModifyMetadata(metadata, attributes);
		if (!metadata.EditorConfiguration.ContainsKey("dirtyCheckInterval"))
		{
			metadata.EditorConfiguration.Add("dirtyCheckInterval", 3000);
		}
	}

	public CustomXhtmlStringEditorDescriptor(ServiceAccessor<TinyMceConfiguration> tinyMceConfiguration) 
	           : base(tinyMceConfiguration)
	{
	}
}

 

How to support a locale with a specific regioncode, for example en-gb, en-nz or en-au

Apparently dojo has issues with their localization modules that makes it impossible to determine the correct culture info when you have two languages from the same locale registered at the same time. 

For example if your editors are from let's say New Zealand the because of this dojo bug https://bugs.dojotoolkit.org/ticket/17155 the language formatting would always fallback to the _ROOT_ nls for them (which is "en" in this case).

It's a very annoying issue as for example number and datetime formatting are different for English editors from US and from GB/NZ/AU.

One could try to solve it by changing the globalization of the whole site or by turning languages in admin mode on and off but in fact the Edit Mode is the only place where we need the change to be visible in.

If you add this simple initializable module to your site it would just affect dojo i18n module without touching the asp.net runtime.

using System;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Shell.UI;
 
namespace AlloyMvcTemplates
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    internal class DateFormatModifier : IInitializableModule
    {
        private void DojoConfigOnSerializing(object sender, EventArgs eventArgs)
        {
            var config = (DojoConfig) sender;
            if (config.Locale.StartsWith("en"))
            {
                config.Locale = "en-gb";
            }
        }
 
        public void Initialize(InitializationEngine context)
        {
            DojoConfig.Serializing += DojoConfigOnSerializing;
        }
 
        public void Uninitialize(InitializationEngine context)
        {
            DojoConfig.Serializing -= DojoConfigOnSerializing;
        }
    }
}

The example above will make sure that all your English editors will always use the "en-gb" language tag so for example date/times would no longer be en-us formatted (default English formatting).

You won't have to change anything about the available locales in the My Settings screen, users can just select English and it would automatically use en-gb instead of the default en-us.

 

If you use the Forms addon then you will also have to do a little trick as the dgrid datetime formatter is set to a fixed pattern 'yyyy-MM-dd'

In order to change it please add the following module:

define([
    "dojo/_base/declare",
    "dojo/date/locale",
    "epi-forms/dgrid/Formatters"
], function (
    declare,
    locale,
    Formatters
) {
    return declare([], {
        initialize: function () {
            Formatters.dateTimeFormatter = function (value) {
                return locale.format(new Date(value), { datePattern: "dd-MM-yyyy", timePattern: "HH:mm:ss" });
            };
        }
    });
});

And register it in your module.config

<clientModule initializer="alloy/formatters-initializer">
  <moduleDependencies>
	<add dependency="EPiServer.Forms.UI" type="RunAfter" />
  </moduleDependencies>
</clientModule>

That way you will also get a custom date formatting in the "Form submisstions" dgrid:

How to create a custom login page in EPiServer MVC with OWIN

 

It's very easy to create a custom login page using the new AspNetIdentityPackage however the biggest problem I encountered was the lack of support for RedirectUrl, user would always be redirected back to the start page.

However, after a bit of digging I made it to work. Please take a look: Custom Login Page

The diff to the current Alloy MVC is here: Custom Login Page Diff

You can just clone my repository and follow the Readme to see it in action.

The problem was to override our default behavior of handling ReturnUrl which we always read from QueryString, however in case of a custom login screen that value is passed in form data. It was easy to do by overriding the default UISignInManager with a custom implementation with an overridden SignIn method that would skip the redirect and let your login controller to do the work.

public class CustomApplicationUISignInManager<T> : ApplicationUISignInManager<T>
  where T : IdentityUser, IUIUser, new()
{
  private readonly ServiceAccessor<ApplicationSignInManager<T>> _signInManager;

  public CustomApplicationUISignInManager(ServiceAccessor<ApplicationSignInManager<T>> signInManager)
    : base(signInManager)
  {
    _signInManager = signInManager;
  }

  public override bool SignIn(string providerName, string userName, string password)
  {
    return _signInManager().SignIn(userName, password, string.Empty);
  }
}

and registration in Startup.cs

// Add CMS integration for ASP.NET Identity
app.AddCmsAspNetIdentity<ApplicationUser>();
app.CreatePerOwinContext<UISignInManager>((options, context) =>
    new CustomApplicationUISignInManager<ApplicationUser>(context
        .Get<ApplicationSignInManager<ApplicationUser>>));

 

 

 

How to run EPiServer from console

Have you ever added admin mode plugins with a single button to run 1-time jobs like import / cleanup / data migration?

Or have you ever added a scheduled job just to run a long running task and see the progress.

Well...I did that plenty of times.

Wouldn't it be easier to just run those kind of 1-time jobs from the console and just forget about stuff like sessions, timeouts, web.config etc.?

Of course, I'm not saying that scheduled jobs or plugins are a bad thing, sometimes there is just no other way. But in most cases, it's easier to use a good old terminal.

A while ago, while visiting the HQ in Stockholm, me & Greg spent a few hours to create a simple tool that would let you do just that.

The idea is simple, you run a console app with a set of parameters:

  • Application Path ( -p ) - path to the folder that contains your webapplication
  • Task Assembly ( -a ) - path to the assembly that contains your custom tasks
  • Tasks ( -t ) - an ordered list of tasks from the Task Assembly

Here it is in action:

As you can see it's easy to use it. You can also report progress back from your tasks if necessary.

The tasks are simple objects with an Execute method. We use duck typing and would only execute those that contain that method.

A basic example of a task is as following:

public class SampleTask1
{
    private readonly IContentRepository _contentRepository;

    public event EventHandler<ProgressChangedEventArgs> Progress;

    public SampleTask1(IContentRepository contentRepository)
    {
        _contentRepository = contentRepository;
    }

    public void Execute()
    {        
        var totalPages = 20;

        for (int i = 1; i < totalPages; i++)
        {                
            var articlePage = _contentRepository.GetDefault<ArticlePage>(ContentReference.StartPage);
            articlePage.Name = "First sample page " + i;
            _contentRepository.Save(articlePage, SaveAction.Publish, AccessLevel.NoAccess);
            Progress?.Invoke(this, new ProgressChangedEventArgs((100 * i)/ totalPages, null));
        }

        Progress?.Invoke(this, new ProgressChangedEventArgs(100, null));
    }
}

It can either be a part of your web application project or it can also be placed in a separate project if you for example don't need to use the model classes.

The code that finds task classes is very simple:

public class TasksLoader
{
    private const string ExecuteMethodName = "Execute";
    private const string ProgressEventName = "Progress";
    private readonly IServiceLocator _serviceLocator;

    public TasksLoader(IServiceLocator serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public IEnumerable<EpiTask> Load(Assembly assembly, IEnumerable<string> tasks)
    {
        var types = assembly.ExportedTypes.Where(t => tasks.Contains(t.Name, StringComparer.InvariantCultureIgnoreCase)).ToList();
        if (types.Count != tasks.Count())
        {
            var missingTaskClasses = tasks.Except(types.Select(t => t.Name));
            throw new ArgumentException($"Invalid task class {string.Join(",", missingTaskClasses)} provided.");
        }
            
        foreach (var task in tasks)
        {
            var type = types.First(t => t.Name == task);
            var executeMethod = type.GetMethod(ExecuteMethodName, BindingFlags.Public | BindingFlags.Instance);
            if (executeMethod == null)
            {
                throw new ArgumentException($"The task {type} is missing the {ExecuteMethodName} method.");
            }

            yield return new EpiTask
            {
                Type = type,
                Instance = _serviceLocator.GetInstance(type),
                ExecuteMethod = executeMethod,
                ProgressEvent = type.GetEvent(ProgressEventName)
            };                
        }            
    }
}

Each of EpiTask instances is then passed to the TaskRunner and executed:

public class TaskRunner
{
    private readonly IServiceLocator _serviceLocator;
    private ProgressBar _pbar;

    public int? tick;

    public TaskRunner(ProgressBar pBar)
    {
        this._pbar = pBar;
    }

    public void Run(EpiTask epiTask)
    {
        if (epiTask.ProgressEvent != null)
        {
            var showProgressMethod = this.GetType()
                .GetMethod("ShowProgress", BindingFlags.Instance | BindingFlags.Public);
            var tDelegate = epiTask.ProgressEvent.EventHandlerType;
            var handler = Delegate.CreateDelegate(tDelegate, this, showProgressMethod);
            epiTask.ProgressEvent.AddEventHandler(epiTask.Instance, handler);
        }
            
        epiTask.ExecuteMethod.Invoke(epiTask.Instance, null);
    }

    public void ShowProgress(object sender, ProgressChangedEventArgs eventArgs)
    {
        if (!tick.HasValue)
        {
            tick = (int)(100 / (eventArgs.ProgressPercentage));
            this._pbar.UpdateMaxTicks(tick.Value);
        }

        this._pbar.Tick();
    }
}

The only tricky part was to initialize the framework. We don't have the web.config in the console app so we have to first point it to the site's web.config and then force it to load the correct episerver.framework section in order to initialize the base framework modules.

HostingEnvironmentMutator.Mutate(AppDomain.CurrentDomain);
Console.WriteLine("Initializing EPiServer environment");

var fileMap = new ExeConfigurationFileMap
{
    ExeConfigFilename = Path.Combine(commandLineParams.AppPath, "web.config")
};
var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
var section = config.GetSection("episerver.framework") as EPiServerFrameworkSection;
ConfigurationSource.Instance = new FileConfigurationSource(config);
InitializationModule.FrameworkInitialization(HostType.Service);

var assembly = Assembly.LoadFrom(commandLineParams.TaskAssembly);
var assemblyName = AssemblyName.GetAssemblyName(commandLineParams.TaskAssembly);
AppDomain.CurrentDomain.Load(assemblyName);

If you use localdb then there's also a little trick that you need to do because the 'DataDirectory' variable is resolved by runtime and we are running our app from the console so we have a problem ;)

Fortunately it's also easy to fix using an instance of SqlConnectionStringBuilder and replacing the connectionString with a real path (without |DataDirectory| variable).

var connectionString = config.ConnectionStrings.ConnectionStrings["EPiServerDB"];
var builder = new SqlConnectionStringBuilder(connectionString.ConnectionString);

if (!string.IsNullOrWhiteSpace(builder.AttachDBFilename))
{
    builder.AttachDBFilename = builder.AttachDBFilename.Replace("|DataDirectory|",
        Path.Combine(commandLineParams.AppPath, "App_Data") + "\\");
}
connectionString.ConnectionString = builder.ConnectionString;

The whole code is available on https://github.com/barteksekula/epiconsole

Customizing TinyMCE Styles Dropdown programatically

I have recently received a question from one of the developers on how to provide a different set of TinyMCE Styles in a multi site environment. 

The expected result was to be able to change both the applied styles in the html editor and the dropdown that is used to choose a particular style.

There is a great post by Arve Systad about Setting editor stylesheets programmatically that is a great starting point for our problem.

We would create an EditorDescriptor where we would place the logic that returns different stylesheets per site.

using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Cms.Shell.UI.ObjectEditing.EditorDescriptors;
using EPiServer.Core;
using EPiServer.Editor.TinyMCE;
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using EPiServer.Web;
using EPiServer.Web.Hosting;

namespace AlloyTemplates.Business.EditorDescriptors
{
    [EditorDescriptorRegistration(TargetType = typeof(XhtmlString), EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
    public class MyEditorDescriptor : XhtmlStringEditorDescriptor
    {
        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            base.ModifyMetadata(metadata, attributes);

            var tinyMceSettings = metadata.EditorConfiguration.FirstOrDefault(x => x.Key == "settings");
            var dictionary = tinyMceSettings.Value as Dictionary<string, object>;
            if (dictionary == null) return;

            var formatter = new TinyMCEStyleFormatter();

            var staticCssEditorCss = SiteDefinition.Current.Name.Equals("Foo") ? "/Static/css/editor.css" : "/Static/css/editor2.css";

            dictionary["content_css"] = staticCssEditorCss;
            using (var stream = GenericHostingEnvironment.VirtualPathProvider.GetFile(staticCssEditorCss).Open())
            {
                var styleFormats = formatter.CreateStyleFormats(stream);
                dictionary["style_formats"] = styleFormats;
            }
        }
    }
}

content_css property is responsible for styling the editor whereas style_formats is responsible for populating the styles dropdown.

Let's imagine we have two separate stylesheets:

  • editor1 - which should be used in the "Foo" site
h2 {EditMenuName:Header 2;color:red;}
h3 {EditMenuName:Header 3;}

h2 {
    color:red;
}
  • editor2 - which should be used in all other sites
h2 {EditMenuName:Alternative Header 2;color:blue;}
h3 {EditMenuName:Alternative Header 3;}
h4 {EditMenuName:Alternative Header 4;}

h2 {
    color:blue;
}

The end result would look as following:

 

And

Annotation rewriter from CMS6 to CMS7

I was given a task to rewrite the annotations in our codebase - we use PageTypeBuilder so each of our page types is defined as following:

[PageType("a653d556-7b52-46e9-87be-83a2a351dc88", AvailableInEditMode = true, AvailablePageTypes = new[] { typeof(BannerPage) })]
public class ArticlePage: PageData {...}

EPiServer upgrade manual says that you should:

  1. Run the upgrade through Deployment Center
  2. Fix compilation errors
  3. Run the solution
However the problem in our case was that CMS is only a part of our enterprise solution and we cannot afford to stop developing new features - most of the team were still actively adding new features to the same codebase.
In order to avoid merging horror I decided to try to stay in trunk for as long as I can and to postpone branching to the very last moment.
My idea was to utilize precompiler directives and conditionally compile different annotations for different CMS versions.
As an example please find the following definition of a page type:
#if EPISERVER_8
[EPiServer.DataAnnotations.ContentType( GUID = "16496e84-3756-44f8-a0a2-a57fc40b9baa")]
[EPiServer.DataAnnotations.AvailableContentTypes(Include = new[] { typeof(CommentPage), typeof(ModuleContainer), typeof(Module) } )]
#endif
#if EPISERVER_6
 [PageType("16496e84-3756-44f8-a0a2-a57fc40b9baa"
,Filename = "~/Templates/Pages/Common/News/NewsPageTemplate.aspx"
,AvailablePageTypes = new [] { typeof(CommentPage), typeof(ModuleContainer), typeof(Module) }
)]
#endif
public class ArticlePage {...}

I had a vision to write a tool that automatically browses through our codebase and transpiles page type builder definitions into those kind of conditional definitions.
I decided to implement it using Roslyn and its powerful implementation of the visitor pattern.
However I found it quite time consuming (I will share some more details in a future post).

Because of the complexity I decided to try with good-old regex ;)

First off we have to scan the source code for page type definitions:

[^/]\[PageType\(("(?<GUID>[^"]+)"\s*,?\s*)?\s*((Name\s*=\s*"(?<Name>[^"]+)"\s*,?\s*)|(Filename\s*=\s*"(?<Filename>[^"]+)"\s*,?\s*)|(SortOrder\s*=\s*(?<SortOrder>\d+)\s*,?\s*)|(DefaultSortIndex\s*=\s*(?<DefaultSortIndex>\d+)\s*,?\s*)|(AvailableInEditMode\s*=\s*(?<AvailableInEditMode>\w+)\s*,?\s*)|(DefaultVisibleInMenu\s*=\s*(?<DefaultVisibleInMenu>\w+)\s*,?\s*)|(AvailablePageTypes\s*=\s*new\s*\[\]\s*{(?<AvailablePageTypes>[^}]+)}\s*,?\s*)|(ExcludedPageTypes\s*=\s*new\s*\[\]\s*{(?<ExcludedPageTypes>[^}]+)}\s*,?\s*)|(\w+\s*=\s*"[^"]+"\s*,?\s*)|(\w+\s*=\s*\S+\s*,?\s*))+\s*\)\s*\]

As you see I defined several named groups to extract all important properties out of the definitions. Next, we have to Regex.Replace

The replacement string is as follows:

#if EPISERVER_8
[EPiServer.DataAnnotations.ContentType(DisplayName = "${Name}", GUID = "${GUID}", AvailableInEditMode = ${AvailableInEditMode}, Order = ${SortOrder})]
[EPiServer.DataAnnotations.AvailablePageTypes(Include = new[] { ${AvailablePageTypes} }, Exclude = new []{${ExcludedPageTypes}} )]
#endif
#if EPISERVER_6
$&
#endif

Analogically you can do the same with properties:

[^/]\[PageTypeProperty\(\s*((EditCaption\s*=\s*@?"(?[^"]+)"\s*,?\s*)|(HelpText\s*=\s*@?"(?[^"]+)"\s*,?\s*)|(DefaultValue\s*=\s*@?"(?[^"]+)"\s*,?\s*)|(Tab\s*=\s*typeof\s*\((?\w+)\)\s*,?\s*)|(Type\s*=\s*typeof\s*\((?\w+)\)\s*,?\s*)|(SortOrder\s*=\s*(?\d+)\s*,?\s*)|(DisplayInEditMode\s*=\s*(?\w+)\s*,?\s*)|(UniqueValuePerLanguage\s*=\s*(?\w+)\s*,?\s*)|(Required\s*=\s*(?\w+)\s*,?\s*)|(\w+\s*=\s*"[^"]+"\s*,?\s*)|(\w+\s*=\s*\S+\s*,?\s*))*\)\]

And the replacement is as follows:

#if EPISERVER_8
[System.ComponentModel.DataAnnotations.Display(
 Name = "${EditCaption}"
 , Description = "${HelpText}"
 , Order = ${SortOrder}
 , GroupName = "${Tab}"
 )]
 [System.ComponentModel.DataAnnotations.Required(${Required})]
 [System.ComponentModel.DataAnnotations.ScaffoldColumn(${DisplayInEditMode})]
 [EPiServer.DataAnnotations.BackingType(typeof(${Type}))]
 [EPiServer.DataAnnotations.CultureSpecific(${UniqueValuePerLanguage})]
#endif
#if EPISERVER_6
$&
#endif

What you have to do is simply enumerate through all project files and try to match/replace

var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var pageModelsPath = Path.Combine(directory, "PageModels");
var enumerable = FilesHelper.GetFiles(pageModelsPath).ToList();
foreach (var file in enumerable)
{
 var code = File.ReadAllText(file);
 var pageRegex = new Regex(RegexExpressions.PageTypeExpression);
 var propertyRegex = new Regex(RegexExpressions.PageTypePropertyExpression);
 if (pageRegex.Match(code).Success == false && propertyRegex.Match(code).Success == false)
 {
 continue;
 }
 code = pageRegex.Replace(code, RegexExpressions.PageTypeReplacementExpression);
 code = propertyRegex.Replace(code, RegexExpressions.PageTypePropertyReplacementExpression);
 ...

Unfortunately this approach generates a lot of noise; regex does not support conditionals so it's not possible to say: 'IF namedGroup[ExcludePageTypes] is not null then ...'. You can simply provide only 1 replacement string.

Thus, I had to do a little cleanup afterwards.

code = new Regex("\\w+\\s*=\\s*\"\"\\s*,?").Replace(code, string.Empty);
code = new Regex("\\[[\\w\\.]+\\(\\)\\]").Replace(code, string.Empty);
code = new Regex("\\[.+\\(\\)\\)\\]").Replace(code, string.Empty);
code = new Regex("\\w+\\s*=\\s*,").Replace(code, string.Empty);
code = new Regex(",\\s*?\\w+\\s*=\\s*\\)").Replace(code, ")");
code = new Regex(",?\\s*\\w+\\s*=\\s*new\\s*\\[\\]\\s*\\{\\s*\\}").Replace(code, string.Empty);
code = new Regex(",\\s*\\)\\]").Replace(code, ")]");

The end results are very satisfactory. The team is still doing new features and I am able to work in the same branch as them doing CMS7+ related stuff.

The only downside is that I needed to add a separate sln/csproj files (blind copies of existing files) that have EPISERVER_8 conditional compilation symbol set.

Ambiguous match found - thrown from ContentData Metadata Provider

I’m in the middle of a big upgrade from CMS6R2 to 7.19.2 and I faced an interesting problem.

It seemed that EPiServer was not able to match interface literal to a valid type. I decided to dig in to EPiServer core to find out what the problem is:

propertyWithAttributes is then passed to a special method that tries to extract the typename and propertyname.

private static PropertyInfo GetPropertyInfoFromInterface(Type modelType, string propertyWithAttributesName)
{
 string[] strArray = propertyWithAttributesName.Split(new char[] { '_' });
 if ((strArray.Length == 2) && (modelType.GetInterface(strArray[0], true) != null))
 ...

As you can see the interface is not fully qualified (it should be episerver.core.icontent_name) and unfortunately we had an interface IContent in our codebase (used for different purposes) which effectively prevented it from working...
After we changed our internal IContent to a different name, everything started to work.