Posted in: Comments

Coming from WebForms development I was a bit annoyed with the inconsistent behavior of the PropertyFor() Html Helper. In WebForms properties are always wrapped with an element, but in MVC this is only true in edit mode. Please see Joel’s excelent post about PropertyFor() and what gets actually rendered to the client.

Conclusion: Properties always gets wrapped with an element in edit mode, but never in view mode. However this is only true for non ContentArea properties, which are not really the problem here.

Why is this a problem you might ask? Well this might actually break your design, especially in edit mode where the extra wrapping element might add styling that you don’t want. So how can we fix this?

One solution is to not use PropertyFor() at all and instead use DisplayFor() or just @Model.YourProperty. But then you need to add the editing attributes manually to a parent element but then that parent will always get rendered in view mode, even though the property doesn’t have a value. You can fix that by adding an if-statement around the parent and the property and then check whether we are in edit mode and/or the property has a value (we always need to render the parent in edit mode). Not so nice, and a lot of unnecessary code and logic in the view:

@if (EPiServer.Editor.PageEditing.PageIsInEditMode || Model.CurrentPage.PageHeading != null)
{
	<h2 @Html.EditAttributes(x => x.CurrentPage.PageHeading)>
		@Html.DisplayFor(x => x.CurrentPage.PageHeading)
	</h2>
}

Replacing the PropertyRenderer was the easist way I found to fix this behavior. Please let me know in the comment section if you have a better way of solving this! It was actually really easy:

public class SitePropertyRenderer : PropertyRenderer
{
    protected override MvcHtmlString GetHtmlForDefaultMode<TModel, TValue>(string propertyName, string templateName, string elementName, string elementCssClass, Func<string, MvcHtmlString> displayForAction)
    {
        if (string.IsNullOrEmpty(elementName))
        {
            return displayForAction(templateName);
        }

        var html = displayForAction(templateName).ToHtmlString();

        if (string.IsNullOrEmpty(html))
        {
            return MvcHtmlString.Empty;
        }

        var tag = new TagBuilder(elementName)
        {
            InnerHtml = html
        };

        if (string.IsNullOrEmpty(elementCssClass) == false)
        {
            tag.AddCssClass(elementCssClass);
        }

        return new MvcHtmlString(tag.ToString());
    }
}

We only have to override  how the property is rendered in view mode and always add the wrapping element. We also need to replace the default renderer in the container with our own, which can be done in an initializable module:

[InitializableModule]
public class ContainerConfigurableModule : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Container.Configure(container =>
            container.For<PropertyRenderer>().Use<SitePropertyRenderer>());
    }
}

Now we can write like this in the view instead, and the wrapper gets always rendered, but only in view mode if the property has a value:

@Html.PropertyFor(x => x.CurrentPage.PageHeading, new { CustomTag = "h2" })

This will probably not be an issue in MVC 6, hopefully in a near future, where we can use Tag Helpers instead.

I have some more blog post in the pipeline on the same topic; keeping the views and output clean. So stay tuned!

Posted in: Comments

This is somewhat of a hidden feature in EPiServer. But there is actually a built-in feature to send a download dialog to the user instead of opening the file directly. Soon the download attribute, that you can set on anchors, will work in all browsers but in the meanwhile we can do:

public static string GetDownloadLink(this ContentReference contentReferenceToMediaFile)
{
    var url = UrlResolver.Current.GetUrl(contentReferenceToMediaFile);

    return url + "/download";
}

The important part here is of course /download. For images you can add /thumbnail (or the name of another blob property) to get the thumbnail of the image.

Posted in: Comments

The answer is very simple, but first your template(s) must meet these client resource requirements. The article mention how to register a script on a page level, and in this case it will be injected on ALL templates that meet the requirements, i.e. page type name is XFormPage.

But let’s say you just want to inject a script if a specific block is used on the page. One solution is to use the method in the article and then iterate through all blocks in the page’s content areas and see if the block is used. Probably not the smartest and fastest solution.

A more simple solution is to require the script in the block’s view or in your controller:

var clientResources = ServiceLocator.Current.GetInstance<IRequiredClientResourceList>();

clientResources.RequireScript("/scripts/my-block-script.js", "MyScript", new[] { "MainScripts" }).AtFooter();

If the script doesn't have any dependencies, you can pass Enumerable.Empty<string>() instead of new[] { "MainScripts" }.

Posted in: Comments

So, I was struggling with creating and updating content in different languages from code. I was able to create language versions of every content for all the languages, but they all ended up with the same content – the content of the first language version, i.e. the master language!

Turned out I was calling Get<T>(ContentReference contentLink, ILanguageSelector selector) in IContentLoader with a ContentReference that had a WorkID to get the current data for updating it. That did of course override the selector parameter I was passing with another language. The content I got back was in the language of the content with that specific WorkID (in this case the master language). So my code just kept updating the master language version and not the language version I was trying to update.

Bottom line; Call Get() with a ContentReference without a WorkID, unless you really want to get that specific version of the content. You can call the extension method ToReferenceWithoutVersion() to construct a ContentReference without a WorkID.

Posted in: Comments

First of all, this is not a new concept but the API has changed since EPiServer 7.14. IUIDescriptorInitializer is now obsolete. So here’s my take on this.

First we need an interface to “mark” the content types with:

namespace DV
{
    /// <summary>
    /// Interface to mark content types as container pages, i.e. changing the icon
    /// in the page tree and defaulting to All Properties view.
    /// </summary>
    public interface IContainerPage
    {
    }
}

Then we need a function to get all types that is marked with this interface, this is a straight copy of Valdis code but I wanted to include it in my post as well to avoid a possible dead link in the future:

namespace DV
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// Helper functions for working with types.
    /// </summary>
    public static class TypeFunctions
    {
        /// <summary>
        /// Get all types that implements or is a subclass of the specified type.
        /// </summary>
        /// <typeparam name="T">The type of the superclass.</typeparam>
        /// <returns>Returns a list of types that implements or is a subclass of the specified type.</returns>
        public static IEnumerable<Type> GetTypesChildOf<T>()
        {
            var types = new List<Type>();

            foreach (var assembly in TypeFunctions.GetAssemblies())
            {
                types.AddRange(TypeFunctions.GetTypesChildOfInAssembly(typeof(T), assembly));
            }

            return types;
        }

        private static IEnumerable<Assembly> GetAssemblies()
        {
            return AppDomain.CurrentDomain.GetAssemblies();
        }

        private static IEnumerable<Type> GetTypesChildOfInAssembly(Type type, Assembly assembly)
        {
            try
            {
                return assembly.GetTypes().Where(t => type.IsAssignableFrom(t) && t.IsClass);
            }
            catch (Exception)
            {
                // there could be situations when type could not be loaded
                // this may happen if we are visiting *all* loaded assemblies in application domain
                return Enumerable.Empty<Type>();
            }
        }
    }
}

Then we only need an initializable module that iterates through all UI descriptors, finds the ones that are marked with the IContainerPage interface and then finally set the default view and icon class.

namespace DV
{
    using System;
    using System.Linq;
    using EPiServer.Framework;
    using EPiServer.Framework.Initialization;
    using EPiServer.Shell;
    using EPiServer.ServiceLocation;

    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class ContainerPageUIDescriptorInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            var containerTypes = TypeFunctions.GetTypesChildOf<IContainerPage>();
            var registry = ServiceLocator.Current.GetInstance<UIDescriptorRegistry>();
            var containerDescriptors = registry.UIDescriptors.Where(d => containerTypes.Contains(d.ForType));

            foreach (var descriptor in containerDescriptors)
            {
                descriptor.DefaultView = CmsViewNames.AllPropertiesView;
                descriptor.IconClass = ContentTypeCssClassNames.Container;
            }
        }

        public void Preload(string[] parameters) { }

        public void Uninitialize(InitializationEngine context)
        {
            // Do nothing.
        }
    }
}