Wednesday, April 26, 2017

EPiServer Custom XForm Column Download Control - Dojo Control


In one of our project, we had requirement to download custom column in csv through XForm. XForm itself has built-in feature to download csv file but it downloads all columns and with existing built-in column such as PostedDate.
Client wanted to download only selected columns even though we were recording lots of information in XForm.

I have built small Dojo Control which enables you to filter data by date using built-in dojo date control and includes columns only that are pre-defined during development. We can extend control to allow to provide additional columns on the fly.

How it is look likes




How to define property


[BackingType(typeof(PropertyXFormCustomColumnDownloadButton))]
[EditorDescriptor(EditorDescriptorType = typeof(XFormCustomColumnDownloadButtonEditorDescriptor))]
[XFormReferencedProperty("Form", "RewardNappyPromoData", "FirstName", "LastName", "AddressLine1", "AddressLine2", "Suburb", "City", "Option")]
[Display(Order = 66)]                                  
public virtual XFormCustomColumnExport XFormDownload { get; set; }


PropertyXFormCustomColumnDownloadButton

This is backing type class to enable the storage of value. We are not saving any value for this as this is just control to download existing records. But we can extend this to store list of columns that need to be exported.


[PropertyDefinitionTypePlugIn(Description = "A property to show button to custom download xfrom column", DisplayName = "Download XForm Data")]
    public class PropertyXFormCustomColumnDownloadButton : PropertyString
    {
        public override Type PropertyValueType
        {
            get { return typeof(XFormCustomColumnExport); }
        }
        public override object SaveData(PropertyDataCollection properties)
        {
            return String;
        }
        public override object Value
        {
            get { return new XFormCustomColumnExport(); }
            set { base.Value = value; }
        }
        public override IPropertyControl CreatePropertyControl()
        {
            //No support for legacy edit mode
            return null;
        }
    }


XFormCustomColumnDownloadButtonEditorDescriptor

This is editor descriptor to let EPiServer know what Js file to load and how to show the control. And set any predefined values that need to be passed to JS file so we can pass back those value from js to server during export.


/// <summary>
    /// Register an editor for XForm custom column download
    /// </summary>
    [EditorDescriptorRegistration(TargetType = typeof(XFormCustomColumnExport))]
    public class XFormCustomColumnDownloadButtonEditorDescriptor : EditorDescriptor
    {
        public const string Form_ID = "FormId";
        public const string Included_Columns = "IncludedColumns";
        public const string XForm_Export_Url = "XFormCustomColumnExportUrl";
        public const string Export_FileName = "ExportFileName";
        public XFormCustomColumnDownloadButtonEditorDescriptor()
        {
            ClientEditingClass = "template.editors.XFormCustomColumnDownload";
        }
        protected override void SetEditorConfiguration(ExtendedMetadata metadata)
        {
            var xFormReferencedPropertyAtt =
                metadata.Attributes.FirstOrDefault(f => f.GetType() == typeof(XFormReferencedPropertyAttribute)) as
                    XFormReferencedPropertyAttribute;
            if (xFormReferencedPropertyAtt != null)
            {
                /* Load xform property to get xform id */
                var propertyData = metadata.Model as PropertyData;
                if (propertyData != null)
                {
                    /* Load the Referenced XForm property to fetch the id */
                    var xForm = propertyData.Parent.GetPropertyValue<XForm>(xFormReferencedPropertyAtt.XFormReferencedPropertyName);
                    if (xForm != null)
                    {
                        EditorConfiguration[Form_ID] = xForm.Id.ToString();
                    }
                    if (xFormReferencedPropertyAtt.IncludedColumns != null &&
                        xFormReferencedPropertyAtt.IncludedColumns.Any())
                    {
                        EditorConfiguration[Included_Columns] = xFormReferencedPropertyAtt.IncludedColumns;
                    }
                    EditorConfiguration[Export_FileName] = xFormReferencedPropertyAtt.ExportFileName;
                }
            }
            EditorConfiguration[XForm_Export_Url] = ConfigurationManager.AppSettings[XForm_Export_Url];
            base.SetEditorConfiguration(metadata);
        }
    }


XFormReferencedPropertyAttribute

This is additional attribute to pass the file name to be exported, Referenced XForm property name from which values will be extracted and columns that need to extracted.


  [AttributeUsage(AttributeTargets.Property)]
  public class XFormReferencedPropertyAttribute : Attribute
  {
      public string ExportFileName;
      public string XFormReferencedPropertyName { get; set; }
      public IList<string> IncludedColumns { get; set; }
      public XFormReferencedPropertyAttribute(string xFormReferncedPropertyName, string exportFileName = null, params string[] includedColumns)
      {
          ExportFileName = exportFileName;
          XFormReferencedPropertyName = xFormReferncedPropertyName;
          IncludedColumns = includedColumns;
      }
  }


XFormCustomColumnExport

This is just place holder property type so EPiServer does not mess up any existing type. I tried to use String but some time it will mess up any existing property with type String.


/// <summary>
/// This class is used for Dojo XFormCustomColumnDownload Control
/// The reason for using seperate class it to make sure dojo control does not mess up other existing types
/// </summary>
public class XFormCustomColumnExport
{
}


Adding Js File and Updating module.config

We need to created Js File inside ClientResources -> Scripts -> Editors
and need to update module.config to let EPiServer know form where to fetch the file.
JS File for Custom XForm Download With Date Controls

template.editors.XFormCustomColumnDownload

the template in name is from module.config, the editors is the folder inside the ClientResources->Scripts folder and XFormCustomColumnDownload is the name of js file.



/*
Dojo widget for downloading custom column of xfrom.
*/
 
define([
    "dojo/_base/declare",
    "dojo/io/iframe",
    "dijit/_CssStateMixin",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetsInTemplateMixin",
    "dijit/form/DateTextBox",
    "epi/shell/widget/_ValueRequiredMixin"
   
],
function (
    declare,
    iframe, /* iframe is required to post download csv file request, Ajax request is not able to download file*/
    _CssStateMixin,
    _Widget,
    _TemplatedMixin,
    _WidgetsInTemplateMixin,
    DateTextBox,
    _ValueRequiredMixin
) {
 
    return declare("template.editors.XFormCustomColumnDownload",
        [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin],
        {
            templateString:
                "<div data-dojo-attach-point=\"containerNode\" class=\"dijit dijitReset dijitInline dijitLeft\">\
                    <form id=\"frmXFormExtract\" Method=\"POST\" />\
                    <input type=\"text\" data-dojo-id=\"startDate\" data-dojo-attach-event=\"onChange:onStartDateChange\" data-dojo-type=\"dijit/form/DateTextBox\" />\
                    <input type=\"text\" data-dojo-id=\"endDate\" data-dojo-attach-event=\"onChange:onEndDateChange\" data-dojo-type=\"dijit/form/DateTextBox\" />\
                    <button type=\"button\" data-dojo-attach-event=\"onclick:onExportButtonClick\" class=\"\">Export</button>\
                </div> ",
 
            formId: null,
            exportFileName: null,
            xFormCustomColumnExportUrl: null,
            includedColumns: null,
 
            baseClass: "XFormCustomColumnDownload",
 
            helptext: "This will download xform values as csv file",
            _deferred: null,
            intermediateChanges: false,
 
            onExportButtonClick: function (e) {
 
                // Cancelling is requried for dojo iframe otherwise dojo iframe will never send any request after 1st request
                // http://stackoverflow.com/questions/13854908/dojo-io-iframe-send-does-not-send-a-request-on-second-time-onwards-in-dojo-1-8
                if (this._deferred) {
                    this._deferred.cancel();
                }
 
                // Dojo iframe sends merged array every time user clicks the export button
                // https://dojotoolkit.org/reference-guide/1.10/dojo/io/iframe.html
                // Iframe use was required to allow download of file otherwise ajax wont download file
                this._deferred = dojo.io.iframe.send({
                    url: this.xFormCustomColumnExportUrl,
                    form: "frmXFormExtract",
                    content: {
                        formId: this.formId,
                        exportFileName: this.exportFileName,
                        StartDate: startDate.toLocaleString(),
                        EndDate: endDate.toLocaleString(),
                        IncludedColumns: this.includedColumns
                    }
                });
        },
             
        onStartDateChange: function (e) {
            startDate.setValue(e);
        },
 
        onEndDateChange: function (e) {
            endDate.setValue(e);
        },
 
        destroy: function () {
        },
        postMixInProperties: function() {
 
            if (this.params.xFormCustomColumnExportUrl) {
                this.xFormCustomColumnExportUrl = this.params.xFormCustomColumnExportUrl;
            }
            if (this.params.formId) {
                this.formId = this.params.formId;
            }
            if (this.params.exportFileName) {
                this.exportFileName = this.params.exportFileName;
            }
            if (this.params.includedColumns) {
                this.includedColumns = this.params.includedColumns;
            }
        },
        postCreate: function () {
            // call base implementation
            this.inherited(arguments);
        }
    });
});


module.config

<?xml version="1.0" encoding="utf-8"?>
<module>
  <assemblies>
    <!-- This adds the current site into the modules" -->
    <add assembly="EPiServer.Reference.Commerce.Site" />
    
  </assemblies>

  <dojoModules>
    <add name="template" path="Scripts" />    
  </dojoModules>

  <clientResources>
    
  </clientResources>

<dojo>
  </dojo></module>

ExportDataController

I have Got ExportDataController under XFormCustomColumnDataExport Area. I am passing relative url to Js File through XFormCustomColumnDownloadButtonEditorDescriptor.


/// <summary>
       /// This function is called by dojo control. Url is also provided in app setting
       /// </summary>
       /// <param name="model"></param>
       /// <returns></returns>
       public FileResult Export(ExportDataModel model)
       {
           if(model == null || model.FormId == Guid.Empty)
               throw new NullReferenceException("Form Id is required");
           var xForm = XForm.CreateInstance(new Guid(model.FormId.ToString()));
           if(xForm == null)
               throw new ContentNotFoundException($"xForm with formId:{model.FormId} could not found");
           /* Fetch all the data within date period */
           var postedData = xForm.GetPostedData(model.StartDate ?? (DateTime) SqlDateTime.MinValue,
               model.EndDate ?? (DateTime)SqlDateTime.MaxValue);
           Func<List<string>, string> csvSafeRow = (rowValues) =>
           {
               return rowValues.Aggregate(string.Empty, (a, b) => $"{a},{b}", s => s.Trim(','));
           };
           /* Adding header */
           StringBuilder exportedData = new StringBuilder(csvSafeRow(model.UniqueIncludedColumns.Select(s => s.AsSafeCsvString()).ToList()));
           exportedData.AppendLine();
           /* Fetch the required column data and append to csv string builder*/
           foreach (var record in postedData)
           {
               var newRow = new List<string>();
               foreach (var header in model.UniqueIncludedColumns)
                   newRow.Add(record.GetValue(header).AsSafeCsvString());
               exportedData.Append(csvSafeRow(newRow));
               exportedData.AppendLine();
           }
           string fileName = string.IsNullOrWhiteSpace(model.ExportFileName) ? "XForm" : model.ExportFileName;
           return File(Encoding.ASCII.GetBytes(exportedData.ToString()), "text/csv", $"{fileName}.csv");
       }


Helper function


   /// <summary>
       /// Removes the New Line(\n,\r) from value and wraps string with Quotes to make it safe for csv file
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static string AsSafeCsvString(this string value)
       {
           if (string.IsNullOrWhiteSpace(value))
               return string.Empty;
           return $"\"{value}\"".Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ");
       }
 

Monday, April 10, 2017

Adding Tax and Jurisdiction in EPiServer Commerce through Code

In one of my project I wanted to add the Tax Jurisdiction, Jurisdiction Group and Tax value through code, instead of editor going through commerce back-end system.

Therefore, i wrote little Migration Step to add Australian Tax Jurisdiction Group, Jurisdiction and Value.


[ServiceConfiguration(typeof(IMigrationStep))]
    public class ImportAustraliaTreasureSiteContent : IMigrationStep
    {
        public bool Execute(IProgressMessenger processMessenger)
        {
            _progressMessenger =  processMessenger;
            _progressMessenger.AddProgressMessageText("Adding/Updating Australia Market, Warehouse, Tax, Shipping and Payment Provider", false, 0);
            try
            {   /* Adding Tax */
                _progressMessenger.AddProgressMessageText("Adding/Updating Australia Jurisdiction and Tax", false, 0);
                AddJurisdictionAndTax(new MarketId("AUS"));

            }
            catch (Exception exception)
            {
                processMessenger.AddProgressMessageText("ImportTreasureSiteContent failed: " + exception.Message + "Stack trace:" + exception.StackTrace, true, 0);
                Log.Fatal(exception.Message, exception);
                return false;
            }
            return true;
        }


In AddJurisdictionAndTax we will first delete any existing australian Tax and Jurisdiction and then will re-add it again.


private static void AddJurisdictionAndTax(IMarket market)
        {
            /* Get Jurisdiction DTO to add/remove jurisdiction  */
            var jurisdictions = JurisdictionManager.GetJurisdictions(JurisdictionManager.JurisdictionType.Tax);

            RemoveAustralianJurisdictionAndTax(jurisdictions, market);
            AddAustralianJurisdictionAndTaxValue(jurisdictions, market);
        }



Removing the Existing Australian Jurisdiction and Tax


  private static void RemoveAustralianJurisdictionAndTax(JurisdictionDto jurisdictions, IMarket market)
        {
            var australiaJurisdictions = jurisdictions.Jurisdiction.Where(w => w.CountryCode == market.MarketId);
            var jurisdictionGroups = JurisdictionManager.GetJurisdictionGroups(JurisdictionManager.JurisdictionType.Tax);

            /* Get relation many to many table for australian jurisdiction */
            var relationsGrp = australiaJurisdictions.SelectMany(s => s.GetJurisdictionRelationRows());

            /* Save id for future jurisdiction group use */
            var relations = relationsGrp.Select(s => new { s.JurisdictionGroupId, s.JurisdictionId }).ToList();
            var ausJuriGrps =
                jurisdictionGroups.JurisdictionGroup.Where(
                    s => relations.Select(g => g.JurisdictionGroupId).Contains(s.JurisdictionGroupId));

            /* Deleting manay to many middle table */
            foreach (var rel in relationsGrp)
            {
                rel.Delete();
            }

            /* Deleting australian jurisdiction */
            foreach (var ausJuri in australiaJurisdictions)
            {
                ausJuri.Delete();
            }


            /* Get JurisfictionsGroup belong to Australian Jurisdictions */
            foreach (var ausJuriGrp in ausJuriGrps)
            {
                ausJuriGrp.Delete();
            }

           /* Saving delete objects */
            JurisdictionManager.SaveJurisdiction(jurisdictions);

            /* Fetching Tax DTO belongs to Australian Language */
            var taxDto = TaxManager.GetTaxDto(TaxType.SalesTax);
            var taxLanguage = taxDto.TaxLanguage.FirstOrDefault(w => w.LanguageCode == market.DefaultLanguage.Name);
            if (taxLanguage != null)
            {
                /* Deleting Australian language and Tax relation */
                taxLanguage.Delete();

                /* Get tax */
                var tax = taxDto.Tax.FirstOrDefault(t => t.TaxId == taxLanguage.TaxId);

                /* Deleting Australian Tax */
                tax.Delete();
            }
            TaxManager.SaveTax(taxDto);
        }


Adding Australian Jurisdiction and Tax


   private static void AddAustralianJurisdictionAndTaxValue(JurisdictionDto jurisdictions, IMarket market)
        {
            /* Add jurisdictions row */
            var jrow = jurisdictions.Jurisdiction.AddJurisdictionRow("Australian Tax", "NSW", market.Countries.FirstOrDefault(),
                 (int)JurisdictionManager.JurisdictionType.Tax, null, null, null, null,
                 null, null, AppContext.Current.ApplicationId, "Australian Tax");

            /* Add jurisdiction group row */
            var jgrow = jurisdictions.JurisdictionGroup.AddJurisdictionGroupRow(AppContext.Current.ApplicationId, "Australian Tax", (int)JurisdictionManager.JurisdictionType.Tax, "Australian Tax");

            /* Add relation between jurisdiction and jurisdiction group */
            var jrela = jurisdictions.JurisdictionRelation.AddJurisdictionRelationRow(jrow, jgrow);

            JurisdictionManager.SaveJurisdiction(jurisdictions);

            /* Add Tax */
            var taxDto = TaxManager.GetTaxDto(TaxType.SalesTax);
            var trow = taxDto.Tax.AddTaxRow((int)TaxType.SalesTax, "Australian Tax", 0, AppContext.Current.ApplicationId);
            var tlrow = taxDto.TaxLanguage.AddTaxLanguageRow("Australian Tax", market.DefaultLanguage.Name,
                trow);
            var tvrow = taxDto.TaxValue.AddTaxValueRow(10d, trow, "Sales", jgrow.JurisdictionGroupId, DateTime.Now,
                Guid.Empty);
            TaxManager.SaveTax(taxDto);
        }



Migraiton Step only runs once in life. So once you initialize a site for the first time. Migration step will kick in and will add tax value.

Wednesday, March 29, 2017

.Net Core Entity Framework Seperate Entity and Mapping Class

Follow the below steps to have separate entity and mapping definition for entity framework.

This blog assumes you have prior knowledge of the .Net Entity Framework and you have ApplicationDBContext created already.

First we will have BaseEntity Class


namespace TogetherWeCan.Data.IdentityManagement.Model
{
    public class EntityBase
    {
    }
}  
  

Then, we will have EntityMappingBase class with virtual function accpeting ModelBuilder as a parameter.


using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Text;

namespace TogetherWeCan.Data.IdentityManagement.Model
{
    public abstract class EntityMappingBase<T> where T : EntityBase
    {

        public virtual void BuildAction(ModelBuilder builder)
        {
            builder.Entity<EntityBase>();
        }

    }
}

We are going to define two Entity Class.

  1. Group
  2. GroupTwo


namespace TogetherWeCan.Data.IdentityManagement.Model
{
    public class Group : EntityBase
    {
        public Guid GroupId { get; set; }
        public string Name { get; set; }
        public DateTime Created { get; set; }
    }
}
namespace TogetherWeCan.Data.IdentityManagement.Model
{
    public class GroupTwo : EntityBase
    {
        public Guid GroupTwoId { get; set; }
        public string Name { get; set; }
        public DateTime Created { get; set; }
    }
}  
  

Next we will define two mapping for Group and GroupTwo entity class.

  1. GroupMapping
  2. GroupTwoMapping


namespace TogetherWeCan.Data.IdentityManagement
{
    public class GroupMapping : EntityMappingBase<EntityBase>
    {
        public override void BuildAction(ModelBuilder builder)
        {
            var entityBuilder = builder.Entity<Group>();
            entityBuilder.HasKey(p => p.GroupId);
            entityBuilder.Property(p => p.Name).IsRequired();

        }

    }
}
  
namespace TogetherWeCan.Data.IdentityManagement
{
    public class GroupTwoMapping : EntityMappingBase<EntityBase>
    {
        public override void BuildAction(ModelBuilder builder)
        {
            var entityBuilder = builder.Entity<GroupTwo>();
            entityBuilder.HasKey(p => p.GroupTwoId);
            entityBuilder.Property(p => p.Name).IsRequired();

        }

    }
}  


Now we will have ApplicationDBContext class to initialize above mapping
In order to get the runtime assembly and class i have used below packages

For RuntimeEnvironment.GetRuntimeIdentifier() I have used  Microsoft.DotNet.InternalAbstractions;

For  DependencyContext.Default.GetRuntimeAssemblyNames(runtimeId).Where(w => w.FullName.Contains("TogetherWeCan.Data")); I have used Microsoft.Extensions.DependencyModel;



using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using TogetherWeCan.Data.IdentityManagement;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
using System.Linq;
using TogetherWeCan.Data.IdentityManagement.Model;
using Microsoft.DotNet.InternalAbstractions;
using Microsoft.Extensions.DependencyModel;

namespace TogetherWeCan.Data
{
    public class ApplicationIdentityDBContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationIdentityDBContext(DbContextOptions<ApplicationIdentityDBContext> options) : base(options)
        {
            
        }
        public DbSet<Group> Group { get; set; }
        public DbSet<GroupTwo> GroupTwo { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            var runtimeId = RuntimeEnvironment.GetRuntimeIdentifier();
            var assemblies = DependencyContext.Default.GetRuntimeAssemblyNames(runtimeId).Where(w => w.FullName.Contains("TogetherWeCan.Data"));

            foreach (var assembly in assemblies)

            {
                Assembly newtonsoftJson = Assembly.Load(assembly);
                foreach (var t in newtonsoftJson.GetTypes())
                {
                    var btyp = t.GetTypeInfo().BaseType;
                    if (btyp != null && btyp.IsConstructedGenericType && btyp.GetGenericTypeDefinition() == typeof(EntityMappingBase<>))
                    {
                        var entityBaseMapping = Activator.CreateInstance(t) as EntityMappingBase<EntityBase>;
                        entityBaseMapping.BuildAction(builder);
                    }

                }

            }
   

            base.OnModelCreating(builder);
        }

    }
}
  
  


Testing

For testing in Startup.cs inject ApplicationDBContext in Configure function and use context.Database.Migrate(); to force entity framework run the migration. Don't forget to add migrations.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TogetherWeCan.Data;
using TogetherWeCan.Data.IdentityManagement;

namespace TogetherWeCan
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddDbContext<ApplicationIdentityDBContext>(options => {
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
                });


            services.AddIdentity<ApplicationUser, IdentityRole>()
       .AddEntityFrameworkStores<ApplicationIdentityDBContext>()
       .AddDefaultTokenProviders();

            // Add framework services.
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ApplicationIdentityDBContext context)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();
            context.Database.Migrate();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
  
  

Saturday, March 18, 2017

Entity Framework Code First Unit/Integration Testing Using Effort

We can unit test Entity Framework Code First using Effort.
Effort can be installed in test project using nuget package https://www.nuget.org/packages/Effort.EF6/ 

Following below steps we can have basic EF code first unit test.

Create Student Domain Class

 
 public class Student
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public int CollegeId { get; set; }
        public DateTime DOB { get; set; }

    }



Create Mapping Class

 
 public class StudentMap : EntityTypeConfiguration
    {
        public StudentMap()
        {

            this.ToTable("Student");

            this.HasKey(s => s.Id);

            this.Property(s => s.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

            this.Property(s => s.Email).IsRequired().HasMaxLength(50);

            this.Property(s => s.FirstName).IsRequired().HasMaxLength(50);

            this.Property(s => s.LastName).IsRequired().HasMaxLength(50);

            this.Property(s => s.DOB).IsRequired();
          
        }
    }



Create School Context Class

Note: There is DatabaseInitializer which is drop and create always required for unit testing.
 
 public class SchoolContext : DbContext
    {
        public SchoolContext() : base()
        {
            
        }

        ///         /// We need this constructor to pass the Effort dbConnection to process data in memory
        ///         public SchoolContext(DbConnection dbConnection) : base(dbConnection, true)
        {
            
            Database.SetInitializer(
      new DropCreateDatabaseAlways());
        }

        public DbSet Student { get; set; }
                
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new StudentMap());

            base.OnModelCreating(modelBuilder);
        }
    }



Create StudentRepository and Student Service class to perform Insert/Update Operations.

IStudentRepository and StudentRepository



public interface IStudentRepository
    {
        Student GetStudent(int studentId);

        int AddStudent(Student student);

    }
 
 public class StudentRepository : IStudentRepository
    {
        private SchoolContext context;

        public StudentRepository(SchoolContext context)
        {
            this.context = context;
        }

        public Student GetStudent(int studentId)
        {
            return this.context.Student.Find(studentId);
        }

        public int AddStudent(Student student)
        {
            this.context.Student.Add(student);
            this.context.SaveChanges();

            return student.Id;
        }
    }



IStudentService and StudentService



 public interface IStudentService
    {
        Student GetStudent(int studentId);

        int AddStudent(Student student);
    }
 
   public class StudentService : IStudentService
    {
        private IStudentRepository _iStudentRepository;

        public StudentService(IStudentRepository iStudentRepository)
        {
            _iStudentRepository = iStudentRepository;
        }

        public Student GetStudent(int studentId)
        {
            return _iStudentRepository.GetStudent(studentId);
        }

        public int AddStudent(Student student)
        {
            return _iStudentRepository.AddStudent(student);
        }

    }



Unit Test Class

Add Effort.EF6 Nuget package to unit test library



 [TestClass]
    public class EntityFrameworkTest
    {
        private SchoolContext context;

        [TestInitialize]
        public void Initialize()
        {
            context = new SchoolContext(Effort.DbConnectionFactory.CreateTransient());
        }


        [TestMethod]
        public void TestConnection()
        {
            IStudentService studentService = new StudentService(new StudentRepository(context));

            studentService.AddStudent(new Student()
            {
                FirstName = "Murtaza",
                LastName = "Ali",
                Email = "murtaza@great",
                DOB = DateTime.Now,
                CollegeId = 75
            });

            var student = studentService.GetStudent(1);


        }
    }


Monday, February 6, 2017

Add Custom Element, Extend Existing Element With Custom Attributes in EPiServer TinyMCE

In one of our project we had to add custom element and custom attributes in TinyMCE for angular directive. However, TinyMCE was stripping off the non-registered element and attributes during the validation

Fortunately, EPiServer provides easy way to Add and Extend TinyMCE element and attributes.

First of all we need to define below class


namespace EPiServer7App.EPiServerCore.Core.TinyMce
{
    [TinyMCEPluginNonVisual(PlugInName = "TinyMceExtendedValidElements", AlwaysEnabled = true, EditorInitConfigurationOptions = "{ custom_elements : 'uib-accordion,uib-accordion-group', extended_valid_elements: 'uib-accordion[close-others],uib-accordion-group[heading]' }")]
    public class TinyMceExtendedValidElements
    {
    }
}



Then, we need to define this EmptyHandler other wise js will throw error


using System.Web;

namespace EPiServer7App.EPiServerCore.Core.TinyMce
{
    public class EmptyFileHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
        }
    }
}


Then, we need to update the web.config location settings

 <location path="util/editor/tinymce/plugins">
    <system.webServer>
      <handlers>
        <add name="TinyMceExtendedValidElements" path="/util/editor/tinymce/plugins/TinyMceExtendedValidElements/editor_plugin.js" verb="GET" type="EPiServer7App.EPiServerCore.Core.TinyMce.EmptyFileHandler, EPiServer7App.EPiServerCore, Version=1.0.0.0, Culture=neutral" />
      </handlers>
    </system.webServer>
  </location>

Then, we will update the episerver.config file to allow TinyMCE merge of custom element.

<episerver xmlns="http://EPiServer.Configuration.EPiServerSection">
  <applicationSettings globalErrorHandling="Off" disableVersionDeletion="false" httpCacheVaryByCustom="path" httpCacheVaryByParams="id,epslanguage" httpCacheability="Public" uiEditorCssPaths="~/Static/css/editor.css" urlRebaseKind="ToRootRelative" pageUseBrowserLanguagePreferences="false" uiShowGlobalizationUserInterface="true" subscriptionHandler="EPiServer.Personalization.SubscriptionMail,EPiServer" uiMaxVersions="20" pageValidateTemplate="false" uiUrl="~/manage/CMS/" utilUrl="~/util/" pageFolderVirtualPathProvider="SitePageFiles" />
  <workflowSettings>
    <workflowHost type="EPiServer.WorkflowFoundation.AspNetWorkflowManager,EPiServer.WorkflowFoundation" />
    <externalServices>
      <externalService type="EPiServer.WorkflowFoundation.Workflows.ApprovalService,EPiServer.WorkflowFoundation" />
      <externalService type="EPiServer.WorkflowFoundation.Workflows.ReadyForTranslationService,EPiServer.WorkflowFoundation" />
      <externalService type="EPiServer.WorkflowFoundation.Workflows.RequestForFeedbackService,EPiServer.WorkflowFoundation" />
    </externalServices>
  </workflowSettings>
  <tinyMCE mergedConfigurationProperties="valid_elements, extended_valid_elements, invalid_elements, valid_child_elements" />
</episerver>


After all of the above steps TinyMCE will not strip off the custom elements and attibutes.

Thursday, February 2, 2017

Creating Split Shipment, Address For Line Item in EPiServer

In one of our project I had to create multiple shipment and addresses depending on the line item type.

The story is, If user includes gift card in the cart then gift card code needs to be sent on email to differnt user. Therefore, i had to add different address to store First Name and Email and add seperate shipment for each gift voucher card.

In order to do that i had to add as many order addresses in cart and for each addresses i had to add shipment and then assign line item to shipment. I also had to add those multiple addresses to order form as well.

Adding Order Address For Each Line Item

 
 foreach (var gift in giftsLineItems)
            {
                var addressId = $"{OrderConstants.GIFT_PREFIX}{gift.LineItem.LineItemId}";
                var giftAddress = giftVoucherOrderAddresses.FirstOrDefault(o => o.Name.Equals(addressId, StringComparison.InvariantCultureIgnoreCase)) ?? _checkoutService.AddNewOrderAddress();
                giftAddress.Name = addressId;
                giftAddress.Email = giftCodeAndEmail[gift.LineItem.LineItemId].Email;
                giftAddress.FirstName = giftCodeAndEmail[gift.LineItem.LineItemId].FirstName;
                giftAddress.CountryCode = _currentMarket.GetCurrentMarket().Countries.First();
                giftAddress.CountryName = _countryManager.GetCountryByCountryCode(giftAddress.CountryCode).Name;
                giftAddress.AcceptChanges();

                /* The shipment code will be described below */
            }


Adding Shipment and Assigning Shipment to Line Item

 
/* Add/Update Shipment based on shipping address id 
 * shipping address is will be unique for each line item as it is based on line item id
 * AddShipmentLineItemShipment functions adds shipment and binds it to line item
 * giftsLineItems is just wrapper for line item giftLineItem object contains Commerce Line Item 
 */
 var shipment = orderForm.Shipments.FirstOrDefault(s => addressId.Equals(s.ShippingAddressId)) ?? orderForm.Shipments.AddNew();
 AddShipmentLineItemShipment(gift.LineItem, shipment, freeShippingEmailDefault, addressId);
 shipment.AcceptChanges();
 gift.LineItem.AcceptChanges();


Adding Shipping Method, Shipping Rate and Assigning it to Line Item

 
 private void AddShipmentLineItemShipment(LineItem lineItem, Shipment shipment, ShippingMethodDto.ShippingMethodRow shippingMethod, string addressId)
        {
            var orderForm = _cartService.GetOrderForms().First();
            var shippingRate = _checkoutService.GetShippingRate(shipment, shippingMethod.ShippingMethodId);

            shipment.ShippingMethodId = shippingMethod.ShippingMethodId;
            shipment.ShippingMethodName = shippingMethod.Name;
            shipment.ShippingAddressId = addressId;
            shipment.SubTotal = shippingRate.Money.Amount;
            shipment.ShippingSubTotal = shippingRate.Money.Amount;

            int lineItemIndex = orderForm.LineItems.IndexOf(lineItem);
            lineItem.ShippingMethodId = shippingMethod.ShippingMethodId;
            lineItem.ShippingMethodName = shippingMethod.Name;
            lineItem.ShippingAddressId = addressId;
            shipment.AddLineItemIndex(lineItemIndex);
        }



GetShippingMethod 

The above method gets the rate for shipping method. This is the function available in QuickSilver template. I have included here as well

  
  public ShippingRate GetShippingRate(Shipment shipment, Guid shippingMethodId)
        {
            var method = ShippingManager.GetShippingMethod(shippingMethodId).ShippingMethod.Single();
            return GetRate(shipment, method);
        }

        private ShippingRate GetRate(Shipment shipment, ShippingMethodDto.ShippingMethodRow shippingMethodRow)
        {
            var type = Type.GetType(shippingMethodRow.ShippingOptionRow.ClassName);
            var shippingGateway = (IShippingGateway)Activator.CreateInstance(type, _currentMarket.GetCurrentMarket());
            string message = null;
            return shippingGateway.GetRate(shippingMethodRow.ShippingMethodId, shipment, ref message);
        }


Sunday, January 29, 2017

Extending EPiServer Commerce Classes

In EPiServer we can extend the existing Commerce Classes. Such as PurchaseOrder, OrderForm and LineItem.

We can use this extended class to use meta class property as class property as oppose to calling object as indexer with hard coded property name.
for e.g


var orderForm = orderGroup.OrderForms[0]
var stockRoomId = orderForm["StockRoomId"]

/* We can use extended class to get value of stock room id */

var tcmsOrderForm = orderGroup.OrderForms[0] as TcmsOrderForm
var stockRoomId = tcmsOrderForm.StockRoomId;



First all of we need to define create the custom meta class and custom meta property in Commerce Section.

We will assign the associated meta property to meta classes.

We will have following custom classes

  • TcmsOrderForm
  • TcmsLineItem

We will also have following meta properties(Not all are included)

  • StockRoomId
  • PeriodId
  • UnitOfMeasure
  • IsPrescribed

Meta properties  will need to be assigned to associated Meta classes in Commerce section.
Once we have meta property and meta classes relation defined we are ready to create extended commerce classes.

Extended Purchase Order (TcmsPurchaseOrder)

using Mediachase.Commerce.Orders;
using System;
using System.Data;
using System.Linq;
using EPiServer7App.EPiServerCore.Utilities;

namespace EPiServer7App.Commerce.MetaFields.Orders
{
    /// <summary>
    /// Extended purchase order class which stores the date sent and date actioned
    /// </summary>
    [Serializable]
    public class TcmsPurchaseOrder : PurchaseOrder
    {
        public const string MetaClass_Name = "PurchaseOrder";

        public const string MetaField_DateActioned = "DateActioned";
        public const string MetaField_DateSent = "DateSent";
        public const string MetaField_OrderCreateLastStep = "OrderCreateLastStep";
        public const string MetaField_TrackingNumberIsFacilityGenerated = "TrackingNumberIsFacilityGenerated";

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsPurchaseOrder"/> class.
        /// </summary>
        public TcmsPurchaseOrder()
        {
            InitializeCustomFields();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsOrderForm"/> class.
        /// </summary>
        /// <param name="reader">The reader.</param>
        public TcmsPurchaseOrder(IDataReader reader)
            : base(reader)
        {
            InitializeCustomFields();
        }


        #endregion

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        protected void InitializeCustomFields()
        {
            DateActioned = null;
            DateSent = null;
            OrderCreateLastStep = Enums.OrderCreateStep.StockTake;
            TrackingNumberIsFacilityGenerated = false;
        }

        /// <summary>
        /// Gets or sets the date actioned.
        /// </summary>
        /// <value>
        /// The date actioned.
        /// </value>
        public DateTime? DateActioned
        {
            get { return this.GetDateTimeValue(MetaField_DateActioned, null); }
            set { this[MetaField_DateActioned] = value; }
        }


        /// <summary>
        /// Gets or sets the date sent.
        /// </summary>
        /// <value>
        /// The date sent.
        /// </value>
        public DateTime? DateSent
        {
            get { return this.GetDateTimeValue(MetaField_DateSent, null); }
            set { this[MetaField_DateSent] = value; }
        }

        /// <summary>
        /// Gets or sets the order create last step.
        /// </summary>
        /// <value>
        /// The order create last step.
        /// </value>
        public Enums.OrderCreateStep OrderCreateLastStep
        {
            get { return (Enums.OrderCreateStep) this.GetIntWithDefaultValue(MetaField_OrderCreateLastStep, (int) Enums.OrderCreateStep.StockTake); }
            set { this[MetaField_OrderCreateLastStep] = (int) value; }
        }

        public bool IsEditable
        {
            get
            {
                var nonEditableStatus = new[]
                {
                    Enums.TcmsOrderStatus.Completed,
                    Enums.TcmsOrderStatus.SubmittedToDistributor,
                    Enums.TcmsOrderStatus.Cancelled
                };

                return !nonEditableStatus.Contains(EnumHelper.ParseOrDefault<Enums.TcmsOrderStatus>(Status));
            }
        }

        /// <summary>
        /// Gets or sets if the tracking number (purchase order number) has been set by the facility as
        /// opposite of being system generated.
        /// </summary>
        public bool TrackingNumberIsFacilityGenerated
        {
            get { return ((bool?) this[MetaField_TrackingNumberIsFacilityGenerated]).GetValueOrDefault(false);}
            set { this[MetaField_TrackingNumberIsFacilityGenerated] = value; }
        }
    }
}

Extended Order Form (TcmsOrderForm)

using System;
using System.Data;
using System.Linq;
using System.Runtime.Serialization;
using EPiServer7App.EPiServerCore.Utilities;
using Mediachase.Commerce.Orders;
using Mediachase.Commerce.Storage;

namespace EPiServer7App.Commerce.MetaFields.Orders
{
    /// <summary>
    /// Extended version of the OrderForm
    /// </summary>
    [Serializable]
    public class TcmsOrderForm : OrderForm
    {
        public const string MetaClass_Name = "OrderFormEx";

        public const string MetaField_StockRoomId = "StockRoomId";
        public const string MetaField_PeriodId = "PeriodId";
        public const string MetaField_Comments = "Comments";

        #region Constructors
        public TcmsOrderForm()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsOrderForm"/> class.
        /// </summary>
        /// <param name="reader">The reader.</param>
        public TcmsOrderForm(IDataReader reader)
            : base(reader)
        {
            InitializeCustomFields();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsOrderForm"/> class.
        /// </summary>
        /// <param name="info">The information.</param>
        /// <param name="context">The context.</param>
        protected TcmsOrderForm(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            InitializeCustomFields();
        } 
        #endregion

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        protected void InitializeCustomFields()
        {
            StockRoomId = Guid.Empty;
            PeriodId = Guid.Empty;
        }

        /// <summary>
        /// Gets or sets the stock room identifier.
        /// </summary>
        /// <value>
        /// The stock room identifier.
        /// </value>
        public Guid StockRoomId
        {
            get { return this.GetGuidValue(MetaField_StockRoomId, Guid.Empty).Value; }
            set { this[MetaField_StockRoomId] = value; }
        }


        /// <summary>
        /// Gets or sets the period identifier.
        /// </summary>
        /// <value>
        /// The period identifier.
        /// </value>
        public Guid PeriodId
        {
            get { return this.GetGuidValue(MetaField_PeriodId, Guid.Empty).Value; }
            set { this[MetaField_PeriodId] = Convert.ToString(value); }
        }

        /// <summary>
        /// Gets or sets the comments.
        /// </summary>
        /// <value>
        /// The comments.
        /// </value>
        public string Comments
        {
            get { return GetString(MetaField_Comments); }
            set { this[MetaField_Comments] = value; }
        }

        protected override void PopulateCollections(DataTableCollection tables, string filter)
        {
            filter = string.Format("OrderFormId = '{0}'", OrderFormId);
            base.PopulateCollections(tables, filter);            
        }

    }
}

Extended Line Item (TcmsLineItem)

using System;
using System.Runtime.Serialization;
using Mediachase.Commerce.Orders;

namespace EPiServer7App.Commerce.MetaFields.Orders
{
    /// <summary>
    /// Extended class for LineItem with TCMS specific values
    /// </summary>
    public class TcmsLineItem : LineItem
    {
        public const string MetaClass_Name = "LineItemEx";

        public const string MetaField_UnitOfMeasure = "UnitOfMeasure";
        public const string MetaField_ActualCatalogEntryId = "ActualCatalogEntryId";
        public const string MetaField_IsPrescribed = "IsPrescribed";
        
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsLineItem"/> class.
        /// </summary>
        public TcmsLineItem()
        {
            Initialize();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TcmsLineItem"/> class.
        /// </summary>
        /// <param name="info">The information.</param>
        /// <param name="context">The context.</param>
        protected TcmsLineItem(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            Initialize();
        } 
        #endregion

        /// <summary>
        /// Initializes this instance with default values
        /// </summary>
        /// <remarks>The base class doesn't expose this as virtual. Sigh!</remarks>
        protected virtual void Initialize()
        {
            UnitOfMeasure = Enums.UnitOfMeasure.Carton;
            IsPrescribed = false;
        }

        /// <summary>
        /// Gets or sets the unit of measure.
        /// </summary>
        /// <value>
        /// The unit of measure.
        /// </value>
        public Enums.UnitOfMeasure UnitOfMeasure
        {
            get { return (Enums.UnitOfMeasure)GetInt(MetaField_UnitOfMeasure); }
            set { this[MetaField_UnitOfMeasure] = (int)value; }
        }
     
        /// <summary>
        /// Gets or sets a value indicating whether [is prescribed].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [is prescribed]; otherwise, <c>false</c>.
        /// </value>
        public bool IsPrescribed
        {
            get { return GetBool(MetaField_IsPrescribed); }
            set { this[MetaField_IsPrescribed] = value; }
        }      
    }
}



ecf.order.config

We also need to update the ecf.order.config section to include the custom class definition

<?xml version="1.0"?>
<Orders newOrderStatus="Draft" autoConfigure="true" shipmentAutoReleaseTimeout="1:0:0">
  <MappedTypes>
    <ShoppingCartType name="Mediachase.Commerce.Orders.Cart,Mediachase.Commerce" />
    
    <!-- Custom Lines -->
    <PurchaseOrderType name="EPiServer7App.Commerce.MetaFields.Orders.TcmsPurchaseOrder,EPiServer7App.Commerce" />
    <OrderFormType name="EPiServer7App.Commerce.MetaFields.Orders.TcmsOrderForm,EPiServer7App.Commerce" />
    <LineItemType name="EPiServer7App.Commerce.MetaFields.Orders.TcmsLineItem,EPiServer7App.Commerce" />    
    <!-- END Custom Lines-->
    
    <!-- ORIGINAL LINES -->
    
    <!--
    <PurchaseOrderType name="Mediachase.Commerce.Orders.PurchaseOrder,Mediachase.Commerce" />
    <OrderFormType name="Mediachase.Commerce.Orders.OrderForm,Mediachase.Commerce" />
    <LineItemType name="Mediachase.Commerce.Orders.LineItem,Mediachase.Commerce" />
    -->

    <PaymentPlanType name="Mediachase.Commerce.Orders.PaymentPlan,Mediachase.Commerce" />    
    <OrderGroupAddressType name="Mediachase.Commerce.Orders.OrderAddress,Mediachase.Commerce" />
    <ShipmentType name="Mediachase.Commerce.Orders.Shipment,Mediachase.Commerce" />
    <OrderSearchType name=" Mediachase.Commerce.Orders.Search.OrderSearch,Mediachase.Commerce" />
  </MappedTypes>
  <MetaClasses>
    <PurchaseOrderClass name="PurchaseOrder" />
    <PaymentPlanClass name="PaymentPlan" />
    <ShoppingCartClass name="ShoppingCart" />]
    <OrderFormClass name="OrderFormEx" />
    <LineItemClass name="LineItemEx" />    
    <ShipmentClass name="ShipmentEx" />
    <OrderAddressClass name="OrderGroupAddressEx" />
  </MetaClasses>
  <Connections confConnectionStringName="EcfSqlConnection" transConnectionStringName="EcfSqlConnection" />
  <Cache enabled="true" shippingCollectionTimeout="0:0:10" paymentCollectionTimeout="0:0:10" statusCollectionTimeout="0:0:10" countryCollectionTimeout="0:0:10" taxCollectionTimeout="0:0:10" jurisdictionCollectionTimeout="0:0:10" />
  <Roles>
    <add name="OrderSupervisorRole" value="Order Supervisor" />
    <add name="OrderManagerRole" value="Order Manager" />
    <add name="ShippingManagerRole" value="Shipping Manager" />
    <add name="ReceivingManagerRole" value="Receiving Manager" />
  </Roles>
</Orders>

After defining custom classes and configuration we are now ready to use the classes
Below are the utility functions and example to use the above classes.


var purchaseOrder = new TcmsPurchaseOrder { TrackingNumber = "DummyTrackingNumber" };

var orderForm = purchaseOrder.OrderForms.AddNew() as TcmsOrderForm;
    
orderForm.LineItems.Add(new TcmsLineItem()
  {
       Quantity = 1,
       ActualCatalogEntryId = 24,
       ExtendedPrice = (decimal)20.30,
       IsPrescribed = true,
       UnitOfMeasure = Enums.UnitOfMeasure.Carton,
       StockTakeQuantityInPieces = 12,
  });
     
     
  //Assumes that we're only using one form!
  return orderGroup.OrderForms[0] as TcmsOrderForm;

Extension method to convert normal classes to extended classes

using System.Collections.Generic;
using EPiServer7App.Commerce.MetaFields.Orders;
using Mediachase.Commerce.Orders;

namespace EPiServer7App.Commerce.Services.Extensions
{
    /// <summary>
    /// Creates extension methods for order group implementations such as Cart and Purchase Order
    /// </summary>
    public static class OrderGroupExtensions
    {
        /// <summary>
        /// Gets the TCMS order form.
        /// </summary>
        /// <param name="orderGroup">The order group.</param>
        /// <returns></returns>
        public static TcmsOrderForm GetTcmsOrderForm(this OrderGroup orderGroup)
        {
            if (orderGroup == null)
                return null;

            if (orderGroup.OrderForms.Count < 1)
                return null;

            //Assumes that we're only using one form!
            return orderGroup.OrderForms[0] as TcmsOrderForm;
        }

        /// <summary>
        /// To the TCMS order form.
        /// </summary>
        /// <param name="orderForm">The order form.</param>
        /// <returns></returns>
        public static TcmsOrderForm ToTcmsOrderForm(this OrderForm orderForm)
        {
            if (orderForm == null)
                return null;

            return orderForm as TcmsOrderForm;
        }

        /// <summary>
        /// To the TCMS line item.
        /// </summary>
        /// <param name="lineItem">The line item.</param>
        /// <returns></returns>
        public static TcmsLineItem ToTcmsLineItem(this LineItem lineItem)
        {
            if (lineItem == null)
                return null;

            return lineItem as TcmsLineItem;
        }
    }
}



Monday, January 23, 2017

Adding EPiServer Meta Property through Code And Assigning it to MetaClass

In this post, we will go through the feature to add meta property to OrderForm, LineItem and CredtiCardPayment class.

We are going to use MetaClass and MetaField class of Mediachase.MetaDataPlus to add/update property to class

 

var metaLineItemClass = MetaClass.Load(MetaDataContext.Instance, "LineItemEx")


The above function will return the instance of LineItemEx  class. Using this instance you can add meta field to give meta class.

 

var metaProp = MetaField.Load(MetaDataContext.Instance, "GiftVoucherCode")



The above function will return the instance of field "GiftVoucherCode" meta property. Do not forget, MetaField are unique and can only be one created but can be assigned to any meta class.


 
if (metaProp == null)
                metaProp = MetaField.Create(MetaDataContext.Instance, "Mediachase.Commerce.Orders", "Gift Voucher Code", "Gift Voucher Code", "Gift Voucher Code", MetaDataType.ShortString, 100, true, false, false, false);
if (!metaLineItemClass.MetaFields.Contains(metaProp))
                metaLineItemClass.AddField(metaProp);


The above piece of code first checks if the meta field is already created. If not then it creates one.
It also checks if the meta field is already associated with meta class then it ignores it else assigns the meta field to meta class using AddField function in meta class.

Below is the complete code that I have used in my project.

We will have class MetaClassAndFieldsSyncInitializer which will add property to meta class.


 using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Reference.Commerce.Shared.Settings;
using Mediachase.MetaDataPlus;
using Mediachase.MetaDataPlus.Configurator;

namespace EPiServer.Reference.Commerce.Shared.Infrastructure.Initialization
{
    public class MetaClassAndFieldsSyncInitializer
    {
        private readonly MetaDataContext _metaDataContext;
        private const string PRODUCT_TYPE = "ProductType";
        private const string LINE_ITEM_EX = "LineItemEx";
        private const string ORDER_FORM_EX = "OrderFormEx";
        private const string ORDER_GROUP_ADDRESS_EX = "OrderGroupAddressEx";
        private const string ORDER_CREDIT_CARD_PAYMENT = "CreditCardPayment";


        private List<string> _productTypes = Enum.GetNames(typeof(Enums.ProductType)).ToList();
            
        public MetaClassAndFieldsSyncInitializer(MetaDataContext metaDataContext)
        {
            if (metaDataContext == null)
                throw new ArgumentNullException("metaDataContext");
            _metaDataContext = metaDataContext;
        }

        public void Sync()
        {
            AddBaseProductProductType();
            AddLineItemFields();
            AddOrderFormFields();
            AddOrderGroupAddressFields();
            AddOrderPaymentFields();
        }

        private void AddBaseProductProductType()
        {
            var metaProp = AddProductTypeField();
        }

        private void AddLineItemFields()
        {
            var metaLineItemClass = MetaClass.Load(_metaDataContext, LINE_ITEM_EX);

            /* Will add them back when nedd them */
            var metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.LineItem.GiftVoucherCode);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders.System.LineItem", MetaFieldsConstants.LineItem.GiftVoucherCode, "Gift Voucher Code", "Gift Voucher Code", MetaDataType.ShortString, 20, true, false, false, false);

            if (!metaLineItemClass.MetaFields.Contains(metaProp))
                metaLineItemClass.AddField(metaProp);

            metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.LineItem.LineItemAbstract);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.LineItem.LineItemAbstract, "LineItem Abstract", "LineItem Abstract", MetaDataType.LongHtmlString, 5000, true, false, false, false);

            if (!metaLineItemClass.MetaFields.Contains(metaProp))
                metaLineItemClass.AddField(metaProp);

            metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.LineItem.ImageUrl);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.LineItem.ImageUrl, "Image Url", "Image Url", MetaDataType.ShortString, 1000, true, false, false, false);

            if (!metaLineItemClass.MetaFields.Contains(metaProp))
                metaLineItemClass.AddField(metaProp);

            metaProp = AddProductTypeField();
            if (!metaLineItemClass.MetaFields.Contains(metaProp))
                metaLineItemClass.AddField(metaProp);
        }

        private void AddOrderGroupAddressFields()
        {
            var metaOrdeGroupAddressClass = MetaClass.Load(_metaDataContext, ORDER_GROUP_ADDRESS_EX);
            var metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.OrderGroupAddress.Suburb_Field);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.OrderGroupAddress.Suburb_Field, "Suburb", "Suburb", MetaDataType.ShortString, 100, true, true, false, false);
            if (metaProp != null && !metaOrdeGroupAddressClass.MetaFields.Contains(metaProp))
                metaOrdeGroupAddressClass.AddField(metaProp);
        }

        private void AddOrderFormFields()
        {
            var metaOrderFormClass = MetaClass.Load(_metaDataContext, ORDER_FORM_EX);
            var metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.OrderForm.SubscribeToNewsletters_Field);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.OrderForm.SubscribeToNewsletters_Field, "Subscribe To NewsLetter", "Subscribe To NewsLetter", MetaDataType.Boolean, 10, true, false, false, false);
            if(metaProp != null && !metaOrderFormClass.MetaFields.Contains(metaProp))
                metaOrderFormClass.AddField(metaProp);

            metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.OrderForm.ReOrdering_Field);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.OrderForm.ReOrdering_Field, "Reordering", "Reordering", MetaDataType.Boolean, 10, true, false, false, false);
            if (metaProp != null && !metaOrderFormClass.MetaFields.Contains(metaProp))
                metaOrderFormClass.AddField(metaProp);
        }

        private void AddOrderPaymentFields()
        {
            var metaOrderFormClass = MetaClass.Load(_metaDataContext, ORDER_CREDIT_CARD_PAYMENT);
            var metaProp = MetaField.Load(_metaDataContext, MetaFieldsConstants.OrderPayment.PaymentProviderResponse_Field);
            if (metaProp == null)
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Orders", MetaFieldsConstants.OrderPayment.PaymentProviderResponse_Field, "Payment Provider Response", "Payment Provider Response", MetaDataType.LongString, 10000, true, false, false, false);
            if (metaProp != null && !metaOrderFormClass.MetaFields.Contains(metaProp))
                metaOrderFormClass.AddField(metaProp);
        }

        private MetaField AddProductTypeField()
        {
            MetaField metaProp = MetaField.Load(_metaDataContext, PRODUCT_TYPE);

            if (metaProp == null)
            {
                metaProp = MetaField.Create(_metaDataContext, "Mediachase.Commerce.Catalog", PRODUCT_TYPE, "Product Type", "Product type such as(Walker, Crawler, Toddler and etc", MetaDataType.EnumSingleValue, 1000, true, false, false, false);
            }

            foreach (var pr in _productTypes)
            {
                var existItem = metaProp.Dictionary.GetItem(pr);
                if (existItem == null)
                    metaProp.Dictionary.Add(pr, pr);

            }

            return metaProp;
        }
    }
}

We will also this SharedDependencyInitialization which will be call once before initialization of site to add/update any meta class or meta field.



using System.Reflection;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Reference.Commerce.Shared.Data.BusinessFoundation.Initialization;
using EPiServer.Reference.Commerce.Shared.Utilities;
using EPiServer.ServiceLocation;
using Mediachase.BusinessFoundation.Data;
using Mediachase.MetaDataPlus;

namespace EPiServer.Reference.Commerce.Shared.Infrastructure.Initialization
{
 [ModuleDependency(typeof(ServiceContainerInitialization))]
 [InitializableModule]
 public class SharedDependencyInitialization : IConfigurableModule
 {
  private bool _isInitialized;

  public void Initialize(InitializationEngine context)
  {
   if (_isInitialized)
    return;

      SyncMetaObjects();

                    _isInitialized = true;
  }

  public void Uninitialize(InitializationEngine context)
  {
   _isInitialized = false;
  }

  public void ConfigureContainer(ServiceConfigurationContext context)
  {
   context.Container.Configure(ctx => ctx.AddRegistry<SharedDependencyRegistry>());
  }

  private static void SyncMetaObjects()
     {
            new MetaClassAndFieldsSyncInitializer(MetaDataContext.Instance).Sync();
        }
 }
}