Download images from assets from HTML Anchor tag

Suppose the front-end code wants supply a way to download images from the assets folder. Consider the following code:

<a href="http://127.0.0.1:5500/globalassets/images/sample.png" download="sample.png">Download this file</a>

Assuming that 127.0.0.1:5500 is your EPiServer instance, you can say that the image will be downloaded.

Note that download anchor attribute will work if the url is from the same origin of the http request.

For Internet Explorer, the download attribute is not supported, so the image is open on the same tab.

We want to ensure that the response header Content-Disposition: attachment; filename="sample.png"; filename*=UTF-8''sample.png is returned, so each browser saves the image instead of present it on the same tab.

Using the DownloadMediaRouter.DownloadSegment on the request, EPiServer will return the image with the Content-Disposition with attachment value.

So, we can replace the html anchor definition by:

<a href="http://127.0.0.1:5500/globalassets/images/sample.png/download">Download this file</a>

Missing Group on Set Access Rights

Context:

  • Episerver authentication integrated with Azure Active Directory B2C;
  • A fresh installation of Episerver;
  • Virtual roles configured to map Roles configured on AD;
  • Logged only with an account that was mapped for a AD role named ‘SpecialMegaRole’ 😉
  • Another virtual role was mapped for an AD role named ‘NotSoSpecialRole’

Objective : Set the access rights for the ‘NotSoSpecialRole’

Problem: The ‘NotSoSpecialRole’ was not been presented at Set Access Rights page.


Possible Solution: After the first login with an AD user that belong to that “NotSoSpecialRole’, the role became available to set the permitions.

Add Metafields to OrderSummary view

I need to add extra information on each LineItem of the order. But also to see it on the OrderSummary view on the Commerce UI. Please comment if i can implement a better solution than following approach, because i thought that it could be a fast solution by changing the xml file were we define the order ui like PurchaseOrder-ObjectView.xml

First, defined the Meta Fields and associated with the LineItemEx:

When creating the LineItem, don’t forget to had the new attribute the Properties collection:

 ILineItem lineItem = cart.CreateLineItem(entryContent.Code, _orderGroupFactory);
 lineItem.DisplayName = entryContent.DisplayName;
 lineItem.Properties[MetaFieldNames.LineItem.ReasonCode] = inventoryRecord.ReasonCode;

Change the CommerceManager/Apps/Order/Modules/OrderSummary.ascx file in order to define a new code behind and add a new BoundColumn with DataField=”ReasonCode” that will bind the new MetaField

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="OrderSummary.ascx.cs" Inherits="MyProjectNamespaceSegment.CommerceManager.Apps.Order.Modules.OrderSummary" %>
<div style="padding:10px;">

	...

 <table cellpadding="0" cellspacing="0" width="100%" style="table-layout:fixed;">
    <tr>
        <td colspan="2" style="border: solid 1px #999999;">
            <asp:DataGrid runat="server" ID="MainGrid" .......>
                <HeaderStyle BackColor="#eeeeee" />
                <Columns>
                    <asp:BoundColumn DataField="OrderFormId".....></asp:BoundColumn>

                    <asp:BoundColumn DataField="ReasonCode" HeaderText="Reason Code" ItemStyle-CssClass="ibn-vb2" HeaderStyle-CssClass="ibn-vh2"></asp:BoundColumn>
                </Columns>
            </asp:DataGrid>
        </td>
    </tr>
</table>

Add to the solution a new OrderSummary.ascx.cs file with the same code has the episerver OrderSummary.ascx.cs existing file. Used ILSpy to get the code from Mediachase.Commerce.Manager.Apps.Order.Modules.OrderSummary.

On the private method BindData, change DataTable colums’ definition to include the new MetaField and the rows added to include LineItem.Properties[New MetaField Name] value.

private void BindData()
{
    DataTable dataTable = new DataTable
    {
        Locale = CultureInfo.InvariantCulture
    };
    dataTable.Columns.Add(new DataColumn("OrderFormId", typeof(int)));
    dataTable.Columns.Add(new DataColumn("LineItemId", typeof(int)));
    dataTable.Columns.Add(new DataColumn("Code", typeof(string)));
...
    dataTable.Columns.Add(new DataColumn(MetaFieldNames.LineItem.ReasonCode, typeof(string)));
....

    using (IEnumerator<IOrderForm> enumerator = orderGroup.Forms.Where((IOrderForm x) => x.Name != OrderForm.ReturnName).GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            IOrderForm current = enumerator.Current;
            foreach (ILineItem allLineItem in current.GetAllLineItems())
            {
                decimal num = orderGroup.Currency.Round(allLineItem.GetEntryDiscountValue());
                decimal num2 = orderGroup.Currency.Round(allLineItem.GetOrderDiscountValue());
                DataRow dataRow = dataTable.NewRow();
                ...
                dataRow[MetaFieldNames.LineItem.ReasonCode] =
                                    allLineItem.Properties[MetaFieldNames.LineItem.ReasonCode];
                ...

Now, i can see the new field on the OrderSummary view:

The view ‘index’ or its master was not found or no view engine supports the searched locations. From IISExpress to IIS

Consider the following scenario: Developing with debug compilation and using IIS Express. Everything works fine without license. Episerver allows development under those circumstances.

But if you deploy to a new IIS site or even to Azure web app and you set compilation without debug, because you probably published with Release configuration:

 <system.web>
    <httpRuntime targetFramework="4.7.2" requestValidationMode="2.0" fcnMode="Single" />
    <compilation targetFramework="4.7.2" optimizeCompilations="true" debug="false"/>

The site will show up with this error:

If you change the compilation for debug

 <system.web>
    <httpRuntime targetFramework="4.7.2" requestValidationMode="2.0" fcnMode="Single" />
    <compilation targetFramework="4.7.2" optimizeCompilations="true" debug="true" />

Now you get the site, but with the episerver warning stating that license does not exists.

Add the license.config file to root of the site or to another folder and update the file EPiServerFramework.config with:

   <licensing licenseFilePath="License.config" />
</episerver.framework>

Refresh the site and is back to work:

Start from empty folder to a running Episerver CMS and ECommerce

Assumptions: this post was written at 23/10/2019 with Visual Studio 2019 and Episerver CMS Visual Studio Extension version 11.5.0.412.

The idea is to create a running empty site with CMS and ECommerce.

Ensure that you have Episerer nuget feed configure to https://nuget.episerver.com/feed/packages.svc/

Lets create a new empty Episerver CMS site:


With this we start to create the CMS web application

Choose empty template:

And the simple site folders and files are created with default values.

Lets update the nuget packages by running the following command at the Package Manager Console:

Update-Package EPiServer.CMS -ToHighestMinor

Do the same for the EPiServer.CMS.UI and EPiServer.CMS.TinyMce packages

Using package manage console and selecting only ToHighestMinor allows Episerver packages to be safely installed because it won’t upgrade to a higher major version that would have breaking changes, and non-Episerver dependencies will be updated, as well as the Episerver ones but it won’t accidently install newer but incompatible packages.

By default, this site is using LocalDb database files for demo purpose. Lets create two database using SQLExpress and SQL Server Management Studio.

Create epidemo.cms and epidemo.commerce database.

Now, it’s time to change the connections string to set the new database. For your new epidemo.cms database we can use the following:

.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true

Create a new file on the root of the project and named connectionStrings.config with the following content:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
</connectionStrings>

Find the connectionStrings section at Web.Config and replace it by:

<connectionStrings configSource="connectionStrings.config" />

You may ask if this is necessary for the post purpose, no its not, but i think is always good to learn something else if you did not know how to split the web.config in multiple files.

Now we have conditions to initialize your database, for now it’s only an empty database:

At Visual Studio Package Manager Console, execute the following command:

initialize-epidatabase

Now, the database was populated with necessary for support Episerver CMS.

After initialize the database, its a good idea to update to the latest changes, by executing the following:

update-epidatabase

In this demo, i don’t want to use Windows Integrated authentication, that is set by default. I would like to use SQL database to store the users.

If you look at the tables that were created at database, there is no support for managing the users. So, let’s change the default provider for membership and roleManager. Find those section at web config and change the defaultProvider to SqlServerMembershipProvider and SqlServerRoleProvider.

With this we use Membership. On another post i will change this to use Identity. Here is a tutorial to use Identity

https://world.episerver.com/documentation/developer-guides/CMS/security/episerver-aspnetidentity/

Since we don’t have yet a user created, we can use an initialization module to create a administrator user with the respective role. So, on Business folder create a new folder named Initialization and a class named RegisterAdminInitializationModule

Add the following code to that class:

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using System.Configuration;
using System.Web.Security;

namespace EpiserverDemo.CMS.Business.Initialization
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class RegisterAdminInitializationModule : IInitializableModule
    {
        private const string roleName = "Administrators";
        private const string userName = "admin";
        private const string password = "store";
        private const string email = "admin@EpiserverAdventures.com";
        public void Initialize(InitializationEngine context)
        {
            string enabledString =
                ConfigurationManager.AppSettings["EpiserverAdventures:RegisterAdmin"];
            bool enabled;
            if (bool.TryParse(enabledString, out enabled))
            {
                if (enabled)
                {
                    #region Use ASP.NET Membership classes to create the role and user
                    // if the role does not exist, create it
                    if (!Roles.RoleExists(roleName))
                    {
                        Roles.CreateRole(roleName);
                    }
                    // if the user already exists, delete it
                    MembershipUser user = Membership.GetUser(userName);
                    if (user != null)
                    {
                        Membership.DeleteUser(userName);
                    }
                    // create the user with password and add it to role
                    Membership.CreateUser(userName, password, email);
                    Roles.AddUserToRole(userName, roleName);
                    #endregion
                }
            }
        }
        public void Uninitialize(InitializationEngine context) { }
    }
}

Change the Web.Config by adding the following to appSettings:

<add key="EpiserverAdventures:RegisterAdmin" value="true" />

Change the membership provider SqlServerMembershipProvider, to allow a password with minimum 5 letter so we could use the password “store”.

minRequiredPasswordLength="5"

By the way, it is also on this provider that is defined which connection string is used.

Build the project and press Ctrl+F5

To pass by this error, add <clear/> right before the add tag for the profile providers in Web.config

Refresh the page and you will check an 404 error page. Thats good, because since we didn’t yet create the root page, we don’t have a landing page.

At the browser url, add the /episerver after the port number and login with “admin” and “store” as password.


Now it’s time to setup the Ecommerce site.

First we can start to add components to the CMS project. Add the nuget package Episerver.Commerce to the CMS

Add also Episerver.Commerce.UI.ManagerIntegration nuget package to the CMS project.

Review the connection strings that were set on the site:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
  
<add name="EcfSqlConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|EcfSqlConnection_137cc4e0.mdf;Initial Catalog=EcfSqlConnection_137cc4e0;Connection Timeout=60;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
</connectionStrings>

Change the EcfSqlConnection connection string to point to the epidemo.commerce database:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
  <add name="EcfSqlConnection" connectionString="Server=.\SQLExpress;Database=epidemo.commerce;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
</connectionStrings>

Now its time to create the new backend site for the Ecommerce.

Create a new empty asp.net web application:

Add to the EpiserverDemo.Commerce project the nuget package Episerver.CommerceManager

Dont cancel the copy of files

For some reason, the project file was not updated:

Include all files into the project, except obj and bin folders

Also add the nuget package EPiServer.ServiceLocation.StructureMap to the Commerce Project.

Copy from the CMS project the connectionString.config into the Commerce project and replace on the Web.Config (Commerce project) the connectionString section with:

<connectionStrings configSource="connectionStrings.config" />

Now run on the Package Manage Console the two commands:

Initialize-EPiDatabase
Update-EPiDatabase 

Now the epidemo.cms and epidemo.commerce have content:

Set the CMS site as start up project and press Ctrl+F5

Login into the episerver (if requested) and click on “Execute all pending steps”.

After migration, go to episerver

For CMS:

For ECommerce:

Now set as startup project the Commerce. Press Ctrl+F5.

If you got the following, please add <clear/> to the Commerce web.config:

Run the Commerce site again: use by default “admin” username and “store” as password

Setting icons for content types

When creating new types instances, such as pages or blocks, we get a lot of help from the CMS UI identifying which type we want.

But, to offer better experience to the editors, it would be much nice that these types could be present with an image.

Episerver recomend 120×90 dimention for each icon, but if you give different it will be fit to that.

Episerver has an attribute named ImageUrlAttribute . Create a derived type that has a default constructor that sets the path to a default image file:

public class StartPageImageUrlAttribute : ImageUrlAttribute
{
  public StartPageImageUrlAttribute() : base("/static/contenticons/StartPage.png")
  {  }

  public StartPageImageUrlAttribute(string path) : base(path)
  {  }
}

Apply this attribute to your content type classes to show a default icon:

[ContentType(DisplayName = "Start Page", GUID = "BD710683-6F9D-4DA5-A2DC-5CFEF74723A8", Description = "Site start page", GroupName = PagesGroups.Content)]
    [StartPageImageUrl]
    public class StartPage : StandardPage
    {
    }

Or you can add the attribute ImageUrl to the class:

[ContentType(DisplayName = "Standard Page", GUID = "1D1A9C91-3F47-4ACE-90E2-06CBDFABC81A", Description = "Non specific page. Base page for all page content.", GroupName = PagesGroups.Content)]
    [ImageUrl("/static/contenticons/StandardPage.png")]
    public class StandardPage : BasePageData
    {
        [CultureSpecific]
        [Display(Name = "Blocks", GroupName = SystemTabNames.Content, Order = 1000)]
        public virtual ContentArea Blocks { get; set; }
    }

After add the images to the site, you can have the fancy icons on each type:

Sharing properties between content types, and action methods between controllers

It is a good idea to provide derived types from the Episerver content types PageData, BlockData and MediaData to share common properties among all the pages on the site. We should also consider to use that also on the controllers.

Considering the class diagram:

We can start to implement the Base[Classes] and Standard and Start page as:

using EPiServer.Core;
using EpiserverAdventures.Core.Constants;
using System.ComponentModel.DataAnnotations;

namespace EpiserverAdventures.Core.ContentTypes
{
    public abstract class BasePageData : PageData
    {
        [Display(Name = "Include in SEO Sitemap", Description = "Include on SEO sitemap.xml", GroupName = PageTabs.Navigation, Order = 1000)]
        public virtual bool IncludeInSitemap { get; set; }
    }
}
using EPiServer.Core;

namespace EpiserverAdventures.Core.ContentTypes
{
    public abstract class BaseBlockData : BlockData
    {

    }
}
using EPiServer.Core;

namespace EpiserverAdventures.Core.ContentTypes
{
    public abstract class BaseMediaData : MediaData
    {

    }
}

Therefore, we can start to create a standard page and define the start page for the site:

using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EpiserverAdventures.Core.Constants;
using System.ComponentModel.DataAnnotations;
using EpiserverAdventures.Core.ContentTypes;

namespace EpiserverAdventures.Core.Pages
{
    [ContentType(DisplayName = "Standard Page", GUID = "1D1A9C91-3F47-4ACE-90E2-06CBDFABC81A", Description = "Non specific page. Base page for content.", GroupName = PagesGroups.Content)]
    public class StandardPage : BasePageData
    {
        [CultureSpecific]
        [Display(Name = "Blocks", GroupName = SystemTabNames.Content, Order = 1000)]
        public virtual ContentArea Blocks { get; set; }
    }
}
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EpiserverAdventures.Core.Constants;

namespace EpiserverAdventures.Core.Pages.StartPage
{
    [ContentType(DisplayName = "Start Page", GUID = "BD710683-6F9D-4DA5-A2DC-5CFEF74723A8", Description = "Site start page", GroupName = PagesGroups.Content)]
    public class StartPage : StandardPage
    {
    }
}

I have decided to create a separated project from the episerver web project as:

Using the BasePageData model, every page will have the property “IncludeInSitemap” and active by default on page creation.

Update an existing page property

Using methods from IContentLoader or IContentRepository to get existing data, it will be returned as read-only version. If you try to update and save it, you will get a ReadOnly exception, although the model has the public set available

Since the API assume that most of the time are read operations, than it shares the instance object on multiple threads. This reduces the amount of short-lived objects and reduce the memory need for the website.

If you want to change programmatic the property, you need to access to a writable clone of the current instance object. Then you can change it and request to save it. The property IsReadOnly gives information about if is a read-only or cloned instance.

The following code, update a property named “MyProperty” with a new text and publish the page with the new content.

var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();

var contentReference = new ContentReference(23123);

var readOnlyPage = contentRepository.Get<ContentPage>(contentReference);

var clonedPageInstance = readOnlyPage.CreateWritableClone<ContentPage>();

clonedPageInstance.MyProperty = "The new value";

contentRepository.Save(clonedPageInstance, SaveAction.Publish, AccessLevel.NoAccess);

The SaveAction is an enum type of save to perform on the page data object

Member nameMember summary
NoneDo not save data.
SaveSave a page, leaving it in a checked out state.
CheckInSave and check in page, creating a new version only if necessary.
PublishPublish page, creating a new version only if necessary.
RejectReject a checked-in page.
ForceNewVersionFlag that is used to force the creation of a new version.
ForceCurrentVersionSave and check in page, always updating the current version
SkipValidationDoes not validate the data against IValidationService
DelayedPublishSave and check in page, creating a new version only if necessary and sets the content as delayed publish.
ActionMaskMask to clear Force… settings from SaveAction
GetOriginalTypeGets the Type of the current object, ensuring that the eventual type that could be generated by a proxy interceptor is ignored.

The AccessLevel determine the minimum access level that the current user must have to save the content.

Member nameMember summary
NoAccessNo access to an item
ReadRead access to an item
CreateCreate access for an item, i e create new items below this item
EditChange / create new versions of this item
DeleteDelete this item
PublishPublish/unpublish items and versions of an item
AdministerSet access rights for an item
FullAccessFull access for an item
UndefinedAccess level not defined.
GetOriginalTypeGets the Type of the current object, ensuring that the eventual type that could be generated by a proxy interceptor is ignored.