Posted in: Comments

This topic comes up from time to time in the forums. I don’t find some of the solutions given completely adequate. They usually just makes the URL absolute by appending the current context’s host name, if present. What we should do instead, is to load the SiteDefinition for the content passed in to our method and then load the host from that definition. The site definition can contain multiple hosts and different kinds as well, e.g. primary host.

This method also works for MediaData.

public static string ContentExternalUrl(this ContentReference contentLink, CultureInfo contentLanguage, bool absoluteUrl)
{
    var result = ServiceLocator.Current.GetInstance<UrlResolver>().GetUrl(
        contentLink,
        contentLanguage.Name,
        new VirtualPathArguments
        {
            ContextMode = ContextMode.Default,
            ForceCanonical = absoluteUrl
        });

    // HACK: Temprorary fix until GetUrl and ForceCanonical works as expected,
    // i.e returning an absolute URL even if there is a HTTP context that matches the content's site definition and host.
    if (absoluteUrl)
    {
        Uri relativeUri;

        if (Uri.TryCreate(result, UriKind.RelativeOrAbsolute, out relativeUri))
        {
            if (!relativeUri.IsAbsoluteUri)
            {
                var siteDefinitionResolver = ServiceLocator.Current.GetInstance<SiteDefinitionResolver>();
                var siteDefinition = siteDefinitionResolver.GetDefinitionForContent(contentLink, true, true);
                var hosts = siteDefinition.GetHosts(contentLanguage, true).ToList();

                var host = hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Primary) 
                    ?? hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Undefined);

                var basetUri = siteDefinition.SiteUrl;

                if (host != null && host.Name.Equals("*") == false)
                {
                    // Try to create a new base URI from the host with the site's URI scheme. Name should be a valid
                    // authority, i.e. have a port number if it differs from the URI scheme's default port number.
                    Uri.TryCreate(siteDefinition.SiteUrl.Scheme + "://" + host.Name, UriKind.Absolute, out basetUri);
                }

                var absoluteUri = new Uri(basetUri, relativeUri);

                return absoluteUri.AbsoluteUri;
            }
        }
    }

    return result;
}

Posted in: Comments

Since my last post I’ve discovered a few issues/bugs with my approach while making rendering of properties more consistent. Unfortunately my method didn’t work that well in preview mode and Content Area properties got wrapped twice!

I couldn’t find an easy way since there is no method to override when properties are rendered in preview mode (these are rendered inside GetHtmlForEditMode()) and no way of telling if the property is a Content Area inside GetHtmlForDefaultMode(). So instead I had to override the PropertyFor() method which made the code much overly complicated and I also had to reflect some private methods from the default PropertyRenderer.

Here is the new complete code:

/// <summary>
///     Overrides the default renderer of all properties, so if we specify a CustomTag(Name) and optionally CssClass they get
///     rendered in view and preview mode as well. The default renderer only wraps the property in edit mode, except for
///     content areas which are always wrapped.
/// </summary>
public class SitePropertyRenderer : PropertyRenderer
{
    private readonly CachingViewEnginesWrapper viewResolver;

    public SitePropertyRenderer(CachingViewEnginesWrapper viewResolver)
    {
        this.viewResolver = viewResolver;
    }

    public override MvcHtmlString PropertyFor<TModel, TValue>(HtmlHelper<TModel> html, string viewModelPropertyName, object additionalViewData, object editorSettings, Expression<Func<TModel, TValue>> expression, Func<string, MvcHtmlString> displayForAction)
    {
        var contextMode = html.ViewContext.RequestContext.GetContextMode();

        // Properties are always wrapped in edit mode, so no need for custom rendering
        if (contextMode == ContextMode.Edit)
        {
            return base.PropertyFor(html, viewModelPropertyName, additionalViewData, editorSettings, expression, displayForAction);
        }

        var routeValueDictionaries = new RouteValueDictionary(additionalViewData);
        var templateName = this.ResolveTemplateName(html, routeValueDictionaries, expression);
        var isContentArea = this.PropertyIsContentArea(html, expression);

        // Content areas are always wrapped, so no need for custom rendering in view mode.
        if (isContentArea)
        {
            return displayForAction(templateName);
        }

        string elementName = null;

        if (routeValueDictionaries.ContainsKey("CustomTag"))
        {
            elementName = routeValueDictionaries["CustomTag"] as string;
        }

        // Correctly spelled property as well, since Episerver probably made a mistake here
        if (routeValueDictionaries.ContainsKey("CustomTagName"))
        {
            elementName = routeValueDictionaries["CustomTagName"] as string;
        }

        string cssClass = null;

        if (routeValueDictionaries.ContainsKey("CssClass"))
        {
            cssClass = routeValueDictionaries["CssClass"] as string;
        }

        return this.GetHtmlForDefaultAndPreviewMode(templateName, elementName, cssClass, displayForAction);
    }

    private MvcHtmlString GetHtmlForDefaultAndPreviewMode(string templateName, string elementName, string cssClass, Func<string, MvcHtmlString> displayForAction)
    {
        // Rely on standard behavior if no element is specified
        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(cssClass) == false)
        {
            tag.AddCssClass(cssClass);
        }

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

    private bool PropertyIsContentArea<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    {
        var contentAreaType = typeof(ContentArea);
        var modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

        return contentAreaType.IsAssignableFrom(modelMetadata.ModelType);
    }

    #region Private methods reflected from base class

    private string ResolveTemplateName<TModel, TValue>(HtmlHelper<TModel> html, RouteValueDictionary additionalValues, Expression<Func<TModel, TValue>> expression)
    {
        var modelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);

        var tag = additionalValues["tag"] as string;

        if (string.IsNullOrEmpty(tag) && modelMetadata != null)
        {
            tag = this.GetTagFromModelMetadata(modelMetadata);
        }

        if (string.IsNullOrEmpty(tag) == false && modelMetadata != null)
        {
            var templateResolver = html.ViewData["templateResolver"] as TemplateResolver ?? ServiceLocator.Current.GetInstance<TemplateResolver>();

            var templateModel = templateResolver.Resolve(
                html.ViewContext.HttpContext,
                modelMetadata.ModelType,
                modelMetadata.Model,
                TemplateTypeCategories.MvcPartialView,
                tag);

            var templateName = this.GetTemplateName(templateModel, html.ViewContext);

            if (string.IsNullOrEmpty(templateName) == false)
            {
                return templateName;
            }
        }

        if (this.DisplayTemplateWithNameExists(html.ViewContext, tag) == false)
        {
            return null;
        }

        return tag;
    }

    private string GetTagFromModelMetadata(ModelMetadata metaData)
    {
        if (metaData == null || metaData.ContainerType == null)
        {
            return null;
        }

        var property = metaData.ContainerType.GetProperty(metaData.PropertyName);

        if (property != null)
        {
            var uIHintAttributes = property.GetCustomAttributes(true).OfType<UIHintAttribute>();

            var uIHintAttribute = uIHintAttributes.FirstOrDefault(a => string.Equals(a.PresentationLayer, "website", StringComparison.OrdinalIgnoreCase));

            if (uIHintAttribute != null)
            {
                return uIHintAttribute.UIHint;
            }

            uIHintAttribute = uIHintAttributes.FirstOrDefault(a => string.IsNullOrEmpty(a.PresentationLayer));

            if (uIHintAttribute != null)
            {
                return uIHintAttribute.UIHint;
            }
        }

        return null;
    }

    private string GetTemplateName(TemplateModel templateModel, ControllerContext viewContext)
    {
        if (templateModel == null)
        {
            return null;
        }

        if (this.DisplayTemplateWithNameExists(viewContext, templateModel.Name) == false)
        {
            return null;
        }

        return templateModel.Name;
    }

    private bool DisplayTemplateWithNameExists(ControllerContext viewContext, string templateName)
    {
        if (string.IsNullOrEmpty(templateName))
        {
            return false;
        }

        var viewEngineResult = this.viewResolver.FindPartialView(viewContext, $"DisplayTemplates/{templateName}");

        if (viewEngineResult == null)
        {
            return false;
        }

        return viewEngineResult.View != null;
    }

    #endregion
}

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" }.