November Happy Hour will be moved to Thursday December 5th.

Fallback on images, best ways to manage them

Vote:
 

So I've kind of got a grasp on fallback for textusing the below for properties in my model:

public override string MetaDescription
{
    get
    {
        var metaDescription = this.GetPropertyValue(p => p.MetaDescription);
 
        return !string.IsNullOrWhiteSpace(metaDescription)
            ? metaDescription
            : this.MainBody.Snippet(200);
    }
    set { this.SetPropertyValue(p => p.MetaDescription, value); }
}

And realised that they don't work with the <EPiServer:Property /> tag and so sorted that in my views.

Now I'm tackling images and it has raised some interesting questions when it comes to rendering an image.
What I have is:

[UIHint(UIHint.Image)]
public virtual ContentReference Image {
    get
    {
        var image = this.GetPropertyValue(p => p.Image);
 
        if (!ContentReference.IsNullOrEmpty(image))
            return image;
 
        if (!ContentReference.IsNullOrEmpty(this.Author))
        {
            var page = this.Author.GetPage<PersonContent>();
            if (page != null)
                return page.Image;
        }
 
        return null;
    }
    set { this.SetPropertyValue(p => p.Image, value); }
}

Now what this is essentially trying to do is default the image to an associated author is one exists, and it works and does the job - but the rendering has been the interesting part.

What I've done is changed from:

<div class="image">
    <EPiServer:Property runat="server" PropertyName="ProfileImage" Width="150" />
</div>

To:

<div class="image">
    <EPiServer:ContentRenderer runat="server" ID="Image" Width="150" />
</div>

And added to my code behind:

if (!ContentReference.IsNullOrEmpty(this.CurrentPage.Image))
    Image.CurrentData = this.CurrentPage.Image.GetContent<MediaData>();
else
    Image.CurrentData = new ImageFile();

Now the problem comes in with the fact previosly this was rendered using my property control (inheriting from PropertyControlBase) and now it renders using the content control (inheriting from ContentControlBase)

Code for each is:

[TemplateDescriptor(TagString = UIHint.Image, Inherited = true, Default = true)]
public partial class ImageReferenceProperty : BasePropertyControl<ContentReference>
{
    private UrlResolver _urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
 
    public ImageFile ImageFile { getset; }
    public string ImageUrl { getset; }
 
    protected void Page_Load(object sender, EventArgs e)
    {
        this.ImageFile = this.CurrentData.GetContent<ImageFile>();
 
        this.ImageUrl = _urlResolver.GetUrl(this.CurrentData);
        if (this.ImageUrl == null)
            return;
 
        if (this.Width != null && this.Width.Value > 0)
            AppendToImageUrl("width"this.Width.Value.ToString());
        if (this.Height != null && this.Height.Value > 0)
            AppendToImageUrl("height"this.Height.Value.ToString());
        if (!string.IsNullOrWhiteSpace(this.Attributes["mode"]))
            AppendToImageUrl("mode"this.Attributes["mode"]);
    }
 
    protected void AppendToImageUrl(string key, string value)
    {
        this.ImageUrl += (this.ImageUrl.Contains("?"? "&" : "?"+ Server.UrlEncode(key) + "=" + Server.UrlEncode(value);
    }
}

[TemplateDescriptor(
    Default = true,
    Inherited = true,
    TemplateTypeCategory = TemplateTypeCategories.UserControl,
    AvailableWithoutTag = true)]
public partial class Image : BaseContentTemplate<ImageFile>
{
    private UrlResolver _urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
 
    public string ImageUrl { getset; }
 
    protected void Page_Load(object sender, EventArgs e)
    {
        this.ImageUrl = _urlResolver.GetUrl(this.CurrentData.ContentLink);
        if (this.ImageUrl == null)
            return;
 
        if (this.Width != null && this.Width.Value > 0)
            AppendToImageUrl("width"this.Width.Value.ToString());
        if (this.Height != null && this.Height.Value > 0)
            AppendToImageUrl("height"this.Height.Value.ToString());
        if (!string.IsNullOrWhiteSpace(this.Attributes["mode"]))
            AppendToImageUrl("mode"this.Attributes["mode"]);
    }
 
    protected void AppendToImageUrl(string key, string value)
    {
        this.ImageUrl += (this.ImageUrl.Contains("?"? "&" : "?"+ Server.UrlEncode(key) + "=" + Server.UrlEncode(value);
    }
}

(I have extra logic in the base classes to get and merge attributes with the parent property or content renderer)

The problem is though that my content control renders was written to cope with how and image could be dragged into a content renderer, which is different to if I'm just rendering an image as part of page i.e. as a property.

Is there a better way to handle image fallback? I'm thinking maybe if I create a copy of the Image inheriting content control using UIHint.Image as the tag that may handle it but was wondering if there was a nicer way without so much duplication.

#87621
Jun 18, 2014 12:04
Vote:
 

I have found another way, this time still using the property tag. I now have:

<div class="image">
    <EPiServer:Property runat="server" ID="Image" PropertyName="Image" Width="150" />
</div>

var prop = new PropertyContentReference(this.CurrentPage.Image);
prop.Name = "Image";
Image.InnerProperty = prop;
Image.Editable = true;
Image.RenderSettings.Tag = UIHint.Image;

I've not done extensive testing but this seem to work, still hoping there is a better way though as it seems a little hacky.

#87625
Jun 18, 2014 13:09
Vote:
 

Since your property is of type ContentReference, you need to create a renderer for that type instead. E.g.:

[TemplateDescriptor(
    Default = true,
    Inherited = true,
    Tags = new[] // We specify as much as possible to make this 
    {            // renderer more specific, so it overrides the default one.
        UIHint.Image, 
        RenderingTags.Edit,
        RenderingTags.Preview,
    })]
public partial class ImagePropertyControl : PropertyContentReferenceControl, IRenderTemplate<ContentReference>
{
    #region Member Fields

    private ImageFile currentImage;

    #endregion

    protected ImageFile CurrentImage
    {
        get
        {
            if (this.currentImage == null)
            {
                var repository = ServiceLocator.Current.GetInstance<IContentRepository>();

                repository.TryGet<ImageFile>(this.ContentLink, out this.currentImage);
            }

            return this.currentImage;
        }
    }

    public override void CreateOnPageEditControls()
    {
        var image = this.GetControl();

        this.Controls.Add(image);

        if (base.PropertyIsEditableForCurrentLanguage())
        {
            base.ApplyEditAttributes();
        }
    }

    public override void CreateDefaultControls()
    {
        var image = this.GetControl();

        this.Controls.Add(image);
    }

    private HtmlImage GetControl()
    {
        var image = new HtmlImage();

        if (this.CurrentImage == null)
        {
            // Set a place holder image so we get the correct ratio on the image in edit mode.
            image.Src = PlaceHolderImageHandler.GetUrlToPlaceholderImage(width, height);

            return image;
        }

        var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();

        image.Src = urlResolver.GetUrl(this.ContentLink);
        image.Alt = this.CurrentImage.AlternativeText;

        return image;
    }
}
#87648
Jun 18, 2014 16:32
Vote:
 

It seems to me this still suffers from the fact that it doesn't get the fall back from the model but rather provides a generic placeholder - unfortunately for my needs the fall back is instance dependant (grabbing an image from the associated author page).

Even if it wasn't instance dependant I couldn't imagine the use of a fallback image that wasn't at least model dependant (i.e. one fallback for a profile image, another for a product image) and unfortunately I think this approach lacks the context to know where exactly it is trying to provide a fall back for.

In fact I'm already achieveing pretty much the same thing by declaring a my ImageReferenceProperty which inherits from PropertyControlBase<ContentReference> with the UIHint.Image tag, and could already implement the same sort of site-wide fallback there.

I think one of the main issues is that the Property control pulls the data of the PropertyDataCollection, which I'm guess is the backing stored use by the page models, this then bypasses any custom getter/setter logic. Perhaps property resolution that is more relection based is the answer.

#87651
Jun 18, 2014 17:33
Vote:
 

You could have a fallback on images based on settings in the GetControl() method. E.g. specifying the fallback in the property control:

<EPiServer:Property PropertyName="Image" runat="server">
	<RenderSettings FallbackPropertyName="Image" FallbackPage="3" />
</EPiServer:Property>

RenderingSettings are available in PropertyControlBase.

By doing it this way, it's not a global fallback and you can change it in every rendering instance.

#87659
Jun 18, 2014 19:23
Vote:
 

Now that's not a bad idea, think I might explore that one further.

#87673
Jun 19, 2014 10:37
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.