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 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

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.