tag:blogger.com,1999:blog-85251609025485251902024-03-24T19:34:50.023+11:00Murtaza Dhari's blogMurtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.comBlogger47125tag:blogger.com,1999:blog-8525160902548525190.post-76288657466316222832020-04-16T01:01:00.001+10:002020-07-24T18:46:54.038+10:00IEnumerable Batch Extension<div dir="ltr" style="text-align: left;" trbidi="on">
Batches the given source into multiple provided size batches.<br />
As we are dealing with IEnumerable and not IList therefore, we need to set the max batches that needs to be created.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, uint size, uint max = byte.MaxValue)
{
var enumerator = source?.GetEnumerator();
if (enumerator == null)
yield break;
IEnumerable<t> CreateBatch()
{
var batch = size;
while (batch-- > 0 && enumerator.MoveNext())
{
yield return enumerator.Current;
}
}
while (max-- > 0)
{
yield return CreateBatch();
}
}
</t></t></t></ienumerable></code>
</pre>
<br />
Usage<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
var batches = GetIEnumerable().Batch(500, 50);
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-91553497848454534732018-08-11T20:28:00.000+10:002018-09-05T10:48:48.824+10:00EPiServer Find Custom Indexing Job For Number of Sites<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our episerver projects we had 20 sites with one database. We are using epi find and epi find indexing job would never finish completely. During the indexing job after indexing couple of sites Epi Find indexing job would crash and stop the whole job.<br />
<br />
It was becoming difficult to run the whole indexing job in one go. Therefore, I came up with other scheduling job to start from the site at which the job was failed instead of restarting from start.<br />
<br />
This is the custom job to start epi find indexing where it left from.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ScheduledPlugIn(GUID = JobId, DisplayName = "Epi Find Incremental Indexing Job", DefaultEnabled = true)]
public class EpiFindIncrementalIndexingJob : ScheduledJobBase
{
private const string LineBreak = "
";
private const string JobId = "7107017B-F443-4E41-893D-62A9FAE69153";
private readonly ILogger logger = LogManager.GetLogger(typeof(ReIndexNewsJob));
private bool stopSignaled;
private IScheduledJobLogRepository scheduledJobLogRepository;
private ISiteDefinitionRepository siteDefinitionRepository;
public EpiFindIncrementalIndexingJob()
{
IsStoppable = true;
siteDefinitionRepository = ServiceLocator.Current.GetInstance<isitedefinitionrepository>();
scheduledJobLogRepository = ServiceLocator.Current.GetInstance<ischeduledjoblogrepository>();
}
public override string Execute()
{
var executeDateTime = DateTime.UtcNow;
using (var locker = DatabaseLock.Lock(Constants.Connections.EPiServerDB, nameof(EpiFindIncrementalIndexingJob), 0))
{
if (!locker.HasLock)
return "Error: failed to start. Job is already running.";
// Call OnStatusChanged to periodically notify progress of job for manually started jobs
OnStatusChanged($"Starting execution of {GetType()}");
var message = new StringBuilder();
var current = SiteDefinition.Current;
var siteDefinitions = siteDefinitionRepository.List().Concat(new SiteDefinition[1] { SiteDefinition.Empty });
try
{
string getNameOfDefinition(SiteDefinition site) => site == SiteDefinition.Empty ? "Global assets and other data" : site.Name;
// Load the cached list of remaining sites to be indexed
var keyValue = ApplicationCache.Get<keyvalueitem>(nameof(EpiFindIncrementalIndexingJob));
if (keyValue == null)
{
keyValue = new KeyValueItem()
{
Key = nameof(EpiFindIncrementalIndexingJob),
Updated = DateTime.Now,
Type = nameof(EpiFindIncrementalIndexingJob),
Value = string.Empty
};
}
var remaining = new HashSet<string>();
foreach (var value in keyValue.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
remaining.Add(value);
if (remaining.IsNullOrEmpty())
siteDefinitions.ToList().ForEach(s => remaining.Add(s.Id.ToString()));
else
siteDefinitions = siteDefinitions.Where(s => remaining.Contains(s.Id.ToString()));
stopSignaled = false;
foreach (SiteDefinition siteDefinition in siteDefinitions)
{
try
{
SiteDefinition.Current = siteDefinition;
var statusReport = new StringBuilder();
// ReIndex the current site (SiteDefinition.Current)
ContentIndexer.ReIndexResult reIndexResult = ContentIndexer.Instance.ReIndex(
status =>
{
if (status.IsError)
{
string errorMessage = status.Message.StripHtml();
if (errorMessage.Length > 0)
statusReport.Append($"{errorMessage}");
}
OnStatusChanged($"Indexing job [{getNameOfDefinition(SiteDefinition.Current)}] [content]: {status.Message.StripHtml()}");
},
() => stopSignaled);
var siteMessage = $"Indexing job [{getNameOfDefinition(SiteDefinition.Current)}] [content]: {reIndexResult.PrintReport().StripHtml().Replace(Environment.NewLine, string.Empty)}{LineBreak}";
AddLogEntry(siteMessage);
message.Append(siteMessage);
if (stopSignaled)
return message.Append("Scheduled job was stopped").ToString();
if (statusReport.Length > 0)
message.Append($"{statusReport.ToString()}{LineBreak}");
// Update list of remaining sites to support resuming indexing, rather than restarting
remaining.RemoveWhere(s => siteDefinition.Id.ToString().Equals(s));
keyValue.Value = string.Join(",", remaining);
ApplicationCache.Set(nameof(EpiFindIncrementalIndexingJob), keyValue, Constants.Cache.OneYear);
}
catch (Exception ex)
{
message.Append($"{ex.ToString()}{LineBreak}");
LogException(ex);
}
}
// HACK: Use reflection to trigger the re-index of external best bets
var bestBetType = typeof(EPiServer.Find.Cms.BestBets.BestBetSelectorExtensions).Assembly.GetTypes()
.SingleOrDefault(x => x.FullName.EndsWith("BestBets.ExternalUrlBestBetHandlers", StringComparison.InvariantCultureIgnoreCase));
if (bestBetType != null)
{
var reIndexMethod = bestBetType.GetMethod("ReindexExternalUrlBestBets", BindingFlags.Static | BindingFlags.NonPublic);
if (reIndexMethod != null)
{
string result = reIndexMethod.Invoke(null, null)?.ToString();
message.Append($"{result}{LineBreak}");
}
}
}
catch (Exception ex)
{
message.Append($"{ex.ToString()}{LineBreak}");
LogException(ex);
}
finally
{
SiteDefinition.Current = current;
}
return message.ToString();
}
}
public override void Stop()
{
stopSignaled = true;
}
private void AddLogEntry(string message)
{
scheduledJobLogRepository.LogAsync(new Guid(JobId), new ScheduledJobLogItem
{
CompletedUtc = DateTime.UtcNow,
Message = message,
Status = ScheduledJobExecutionStatus.Succeeded,
}).GetAwaiter().GetResult();
}
private void LogException(Exception exception)
{
logger.Critical($"Exception occurred in {nameof(EpiFindIncrementalIndexingJob)}", exception);
}
}
}
</string></keyvalueitem></ischeduledjoblogrepository></isitedefinitionrepository></code>
</pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-68462180889851906552018-05-08T16:41:00.002+10:002018-05-08T16:41:34.178+10:00Asynchronously Throttling EPiFind Request To Stay under EPiFind Limit On Azure<div dir="ltr" style="text-align: left;" trbidi="on">
<h3 style="text-align: left;">
<span style="color: red;">ThrottledSemaphore</span></h3>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <summary>
/// Throttled semaphore to allow maximum number of request concurrently per time period.
/// </summary>
/// <remarks>
/// <para>
/// In order to allow N no. of request concurrently for given time period <see cref="ThrottledSemaphore"/>
/// the caller should call the <see cref="TryLock"/> thorugh <see cref="using"/> as TryLock returns the IDisposable <see cref="ThrottledLock"/>
/// which tells caller whether the lock <see cref="ThrottledLock.IsLocked"/> has been taken or not.
/// </para>
/// <para>
/// <code>
/// private static ThrottledSemaphore throttledSemaphore = new ThrottledSemaphore();
/// using (var @lock = await throttledSemaphore.TryLock().ConfigureAwait(false))
/// {
/// if (@lock.IsLocked)
/// return query.Track().Skip((page - 1) * count).Take(count).GetResult();
/// throw new HttpResponseException((HttpStatusCode)429);
/// }
/// </code>
/// </para>
/// </remarks>
public class ThrottledSemaphore
{
private ConcurrentQueue<DateTime> times;
private SemaphoreSlim semaphore;
public int MaxConcurrency;
private TimeSpan Period;
public ThrottledSemaphore(TimeSpan period, int maxConcurrency = 20)
{
Period = period;
MaxConcurrency = maxConcurrency;
semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
times = new ConcurrentQueue<DateTime>();
}
public async Task<ThrottledLock> TryLock(int timeoutMilliseconds = 500)
{
if (await semaphore.WaitAsync(timeoutMilliseconds).ConfigureAwait(false))
{
await Wait().ConfigureAwait(false);
return new ThrottledLock(ReleaseLock) { IsLocked = true };
}
return new ThrottledLock(null);
}
private void ReleaseLock()
{
lock (semaphore)
{
times.Enqueue(DateTime.UtcNow);
semaphore.Release();
}
}
private async Task Wait()
{
var now = DateTime.UtcNow;
var lastTime = DateTime.MinValue;
if (times.Count >= MaxConcurrency)
times.TryDequeue(out lastTime);
var until = lastTime.Add(Period);
if (MaxConcurrency - semaphore.CurrentCount + times.Count >= MaxConcurrency && until > now)
{
await Task.Delay(until - now).ConfigureAwait(false);
}
}
/// <summary>
/// The disposable releaser tasked with releasing the semaphore.
/// </summary>
public sealed class ThrottledLock : IDisposable
{
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
public bool IsLocked { get; set; }
public delegate void TaskDisposeCallBack();
/// <summary>
/// Task dispose call back to release or dispose any resources requried for this runner.
/// </summary>
private TaskDisposeCallBack taskDisposeCallBack;
public ThrottledLock(TaskDisposeCallBack callBack)
{
taskDisposeCallBack = callBack;
}
/// <summary>
/// Finalizes an instance of the <see cref="ThrottledLock"/> class.
/// </summary>
~ThrottledLock()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
this.Dispose(false);
}
/// <summary>
/// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
/// </summary>
public void Dispose()
{
this.Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">
/// If true, the object gets disposed.
/// </param>
private void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
taskDisposeCallBack?.Invoke();
}
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
}
}
}
</code></pre>
<br />
<br />
<h3 style="text-align: left;">
<span style="color: red;">Content Search Service/Usage</span></h3>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public static class ContentSearchService
{
private static IClient client = SearchClient.Instance;
private static ThrottledSemaphore searchThrottle;
static ContentSearchService()
{
// Throttle the user search requests to to avoid exceeding our allowed requests/sec to Episerver Find
searchThrottle = new ThrottledSemaphore(
period: TimeSpan.FromSeconds(1),
maxConcurrency: CalculateSearchConcurrency(ServerStateEvent.GetServerCount()));
ServerStateEvent.OnServerCountChanged += count => searchThrottle.MaxConcurrency = CalculateSearchConcurrency(count);
}
public static async Task<UnifiedSearchResults> GetSearchResults(string keyword, IList<Type> types = null, SortBy sort = SortBy.Relevance, DateTime? before = null, int page = 1, int count = 24)
{
var query = client.UnifiedSearch().For(keyword) as ITypeSearch<ISearchContent>;
// Filter for specific page types
if (!types.IsNullOrEmpty())
query = query.FilterByExactTypes(types);
// Filter before a specific date time
if (before.HasValue)
query = query.Filter(t => t.SearchPublishDate.Before(before.Value.RoundToMinute(RoundingDirection.Floor)));
switch (sort)
{
case SortBy.Earliest:
query = query.OrderBy(o => o.SearchPublishDate);
break;
case SortBy.Latest:
query = query.OrderByDescending(o => o.SearchPublishDate);
break;
default:
query = query
.BoostMatching(s => s.SearchTitle.Match(keyword), 2)
.BoostMatching(s => s.SearchSection.Match(keyword), 1.5);
break;
}
using (var @lock = await searchThrottle.TryLock().ConfigureAwait(false))
{
if (@lock.IsLocked)
return query.Track().Skip((page - 1) * count).Take(count).GetResult();
throw new HttpResponseException((HttpStatusCode)429);
}
}
private static int CalculateSearchConcurrency(int serverCount)
{
// Divide the maximum concurrent user searches between the active servers (excluding the CMS admin server).
return serverCount > 1 ? (Settings.Search.MaxConcurrency / (serverCount - 1)) : 1;
}
}
</code></pre>
<br />
<br />
<h3 style="text-align: left;">
<span style="color: red;">Monitoring Azure Servers Turning On and Shutting down</span></h3>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public static class ServerStateEvent
{
// HACK: Guid copied from EPiServer.Events.Clients.Internal.ServerStateService.StateEventId and may be subject to change without notice
private static readonly Guid EventId = new Guid("{51da5053-6af8-4a10-9bd4-8417e48f38bd}");
private static readonly Guid RaiserId = Guid.NewGuid();
private static Event serverEvent;
private static ILogger logger = LogManager.GetLogger(typeof(ServerStateEvent));
private static IServerStateService stateService = LicensingServices.Instance.GetService<IServerStateService>();
private static int? serverCount = null;
/// <summary>
/// Event occurs when there has been a change to the number of server instances running in the application
/// </summary>
public static event Action<int> OnServerCountChanged;
public static void Register()
{
var registry = ServiceLocator.Current.GetInstance<IEventRegistry>();
serverEvent = registry.Get(EventId);
serverEvent.Raised += OnRaised;
}
public static int GetServerCount()
{
if (serverCount == null)
serverCount = stateService.ActiveServers();
return serverCount.Value;
}
private static void OnRaised(object sender, EventNotificationEventArgs args)
{
if (args.Param is StateMessage message && (message.Type == StateMessageType.Hello || message.Type == StateMessageType.Bye))
{
// TODO: Remove when logging is removed
var serverCountBefore = serverCount;
// Calculate the new server count and fire the change event
serverCount = stateService.ActiveServers();
OnServerCountChanged?.Invoke(serverCount.Value);
// TODO: Remove logging when confident that event is firing when needed
logger.Information($"A ServerStateEvent was raised. Application: {message.ApplicationName} | Type: {message.Type} | Servers Before: {serverCountBefore} | Servers After: {serverCount}");
}
}
}
</code></pre>
<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[InitializableModule]
[ModuleDependency(typeof(EventsInitialization))]
public class ServerStateConfig : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
ServerStateEvent.Register();
}
public void Uninitialize(InitializationEngine context) { }
}
</code></pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-52449249892989990142018-05-06T22:54:00.001+10:002018-09-28T09:37:35.072+10:00FIFO Semaphore<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our project requirement we had to built the max concurrency requirement for which we decided to use Semaphore but we had requirement to build FIFO Semaphore as by default Semaphore does not guarantee FIFO access to waiting thread.<br />
<br />
Therefore, I have use ConcurrentQueue together with Semaphore to come with FIFO Sempahore<br />
<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Shared.Locking
{
public class ThreadPoolAsyncLock
{
private ConcurrentQueue<(SemaphoreSlim Semaphore, DateTime Time)> queue = new ConcurrentQueue<(SemaphoreSlim, DateTime)>();
private (DateTime start, int requests) requestTime = (DateTime.UtcNow, 0);
private SemaphoreSlim semaphoreSlims;
public static TimeSpan Timeout = TimeSpan.FromMilliseconds(500);
public static int MaxRequest = 30;
public ThreadPoolAsyncLock(int maxThread = 30)
{
semaphoreSlims = new SemaphoreSlim(maxThread, maxThread);
}
public async Task<TaskRunner<TResult>> ScheduleTask<TResult>()
{
/* This lock will make sure no one enter in main semaphore unless some one from main semaphore releases this */
await AcquireOrQueue().WaitAsync(Timeout).ConfigureAwait(false);
/* Main semaphore wait */
if (await semaphoreSlims.WaitAsync(Timeout).ConfigureAwait(false))
{
new TaskRunner<TResult>(ReleaseLock) { IsLocked = true };
}
return new TaskRunner<TResult>(ReleaseLock);
}
private SemaphoreSlim AcquireOrQueue()
{
SemaphoreSlim slim = null;
lock (semaphoreSlims)
{
if (semaphoreSlims.CurrentCount > 0 && (DateTime.UtcNow.Subtract(requestTime.start).Seconds > 1 || requestTime.requests < MaxRequest))
{
slim = new SemaphoreSlim(1, 1);
Interlocked.Increment(ref requestTime.requests);
}
else
{
slim = new SemaphoreSlim(0, 1);
queue.Enqueue((slim, DateTime.UtcNow));
}
}
return slim;
}
/// <summary>
/// Dequeue the thread waiting to enter main semaphore
/// </summary>
/// <remarks>
/// It only allows the dequeue if the main semaphore is not full Or Max requests hasn't been reached within last second.
/// </remarks>
private void DeQueue()
{
lock (semaphoreSlims)
{
if (semaphoreSlims.CurrentCount == 0 || (requestTime.requests >= MaxRequest && DateTime.UtcNow.Subtract(requestTime.start).Seconds <= 1))
return;
Interlocked.Decrement(ref requestTime.requests);
if (queue.TryDequeue(out var semaphore))
{
semaphore.Semaphore.Release();
requestTime.start = semaphore.Time;
}
}
}
private void ReleaseLock()
{
lock (semaphoreSlims)
{
semaphoreSlims.Release();
DeQueue();
}
}
/// <summary>
/// The disposable releaser tasked with releasing the semaphore.
/// </summary>
public sealed class TaskRunner<TResult> : IDisposable
{
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
public bool IsLocked { get; set; }
/// <summary>
/// A Task to run after acquiring and locking the thread.
/// </summary>
/// <remarks>
/// If the entity acquires the lock, then entity shoule call <see cref="RunAsycn" /> or <see cref="Run"/> to run the task />
/// </remarks>
private Task<TResult> task;
public delegate void TaskDisposeCallBack();
/// <summary>
/// Task dispose call back to release or dispose any resources requried for this runner.
/// </summary>
private TaskDisposeCallBack taskDisposeCallBack;
public TaskRunner(TaskDisposeCallBack callBack)
{
taskDisposeCallBack = callBack;
}
/// <summary>
/// Finalizes an instance of the <see cref="TaskRunner{TResult}"/> class.
/// </summary>
~TaskRunner()
{
// Do not re-create Dispose clean-up code here.
// Calling Dispose(false) is optimal in terms of
// readability and maintainability.
this.Dispose(false);
}
/// <summary>
/// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
/// </summary>
public void Dispose()
{
this.Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">
/// If true, the object gets disposed.
/// </param>
private void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
taskDisposeCallBack?.Invoke();
}
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
}
}
}
}
</code></pre>
<br />
<br />
Usage<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
using (var @lock = await new ThreadPoolAsyncLock().ScheduledTask().ConfigureAwait(false))
{
if (@lock.IsLocked)
return query.Track().Skip((page - 1) * count).Take(count).GetResult();
throw new HttpResponseException((HttpStatusCode)429);
}
</code></pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com6tag:blogger.com,1999:blog-8525160902548525190.post-91253215958270767212018-05-06T22:15:00.001+10:002018-05-06T22:15:13.662+10:00EPiFind Whole Word Matching<div dir="ltr" style="text-align: left;" trbidi="on">
By Default EPiFind tokenizes the query word. If the word is composed of multiple words and we need to search the whole word instead of tokenized words then its bit tricky. We found it bit difficult initially to search for whole given word.<br />
<br />
At the end we figure out to use the regex to specifically tell EPiFind to match whole word instead of tokenizing.<br />
<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">query = query.Search(x => x.For(legacyVideoId, q =>
{
q.Query = $"*{legacyVideoId}*";
}).InField(a => a.LegacyVideoId));
</code></pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-70392219510104011562018-04-17T20:28:00.000+10:002018-04-17T20:28:36.813+10:00Web API Default Behaviour Overriding<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<h2 style="text-align: left;">
<span style="color: red;">Overriding Text Encoding</span></h2>
In one of our project we had an issue where one of invalid text character in web api was throwing exception instead of removing or replacing that bad character. After investigating we figure out that the default Encoding was UTF8 but EncoderFallback was set to throw Exception for invalid character instead of replacing with default Character.<br />
<br />
Therefore, we just added this one below line to replace the text encoding behaviour<br />
<br />
<b>config.Formatters.JsonFormatter.SupportedEncodings[0] = Encoding.UTF8;</b><br />
<br />
The default system UTF8 Encoding fallback set to replace invalid character with "?"<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public static void ConfigureWebApi(HttpConfiguration config)
{
...
...
// HACK: To allow JSON serialization to handle invalid characters
config.Formatters.JsonFormatter.SupportedEncodings[0] = Encoding.UTF8;
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
config.Services.Replace(typeof(IExceptionLogger), new GlobalExceptionLogger());
}</code></pre>
<h2 style="text-align: left;">
<span style="color: red;">Overriding GlobalExceptionHandler</span></h2>
We are using Azure Telemetry to log all of our errors therefore, we wanted to override the global exception handler to log all errors to telemetry. This is similar concept of Global.ascx function for web api.<br />
<br />
the below line will replace the GlobalExceptionHandler for WebApi<br />
<br />
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());<br />
<div>
<br /></div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
var exception = context.Exception;
var message = context.Exception.GetType().Name + ": " + context.Exception.Message;
if (exception is HttpException httpException)
{
context.Result = new CustomErrorResult(context.Request, (HttpStatusCode)httpException.GetHttpCode(), message);
return;
}
// Return HttpStatusCode for other types of exception.
context.Result = new CustomErrorResult(context.Request, HttpStatusCode.BadRequest, message);
}
}
</code>
</pre>
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class CustomErrorResult : IHttpActionResult
{
private readonly string _errorMessage;
private readonly HttpRequestMessage _requestMessage;
private readonly HttpStatusCode _statusCode;
public CustomErrorResult(HttpRequestMessage requestMessage, HttpStatusCode statusCode, string errorMessage)
{
_requestMessage = requestMessage;
_statusCode = statusCode;
_errorMessage = errorMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_requestMessage.CreateErrorResponse(_statusCode, _errorMessage));
}
}
</code>
</pre>
<h2 style="text-align: left;">
<span style="color: red;"><br />Overriding GlobalExceptionLogger</span></h2>
We also wanted to replace the GlobalExceptionLogger to be our ExceptionLogger to push all errors to Azure Telemetry.<br />
<br />
config.Services.Replace(typeof(IExceptionLogger), new GlobalExceptionLogger());<br />
<div>
<br /></div>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class GlobalExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
new TelemetryClient().TrackException(context.Exception);
}
}
</code>
</pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-61199475704729539892017-12-19T23:58:00.001+11:002018-05-07T09:56:52.741+10:00Azure Storage Account File Storage Wrapper For Basic Functions<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our project we had to upload/download files to azure file storage.<br />
Therefore, I build this small wrapper classes to make use of Azure file storage easy.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">AzureStorageAccountClient</span></h2>
This client is supposed to provide function to connect to different storage account such as (Blob, File and etc storage)<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class AzureStorageAccountClient
{
private readonly ILogger logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public AzureStorageAccountClient(string connectionString)
{
ConnectionString = connectionString;
}
public string ConnectionString { get; }
public IAzureStorageShareClient FileClientConnect()
{
string message = string.Empty;
try
{
return new AzureStorageShareClient(CloudStorageAccount.Parse(ConnectionString).CreateCloudFileClient());
}
catch (Exception ex)
{
logger.Critical("Error in Azure Client", ex);
throw;
}
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">AzureStorageShareClient</span></h2>
This class provides functions to perform on share storage client.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public interface IAzureStorageShareClient
{
CloudFileClient CloudFileClient { get; }
}
public class AzureStorageShareClient : IAzureStorageShareClient
{
public AzureStorageShareClient(CloudFileClient cloudFileClient)
{
CloudFileClient = cloudFileClient;
}
public CloudFileClient CloudFileClient { get; }
}
</code>
</pre>
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">AzureStorageFileDirectoryClient</span></h2>
This class provides functionality to different Directoy related operations.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public interface IAzureStorageFileDirectoryClient
{
CloudFileShare CloudFileShare { get; }
CloudFileDirectory CloudFileDirectory { get; set; }
}
public class AzureStorageFileDirectoryClient : IAzureStorageFileDirectoryClient
{
public AzureStorageFileDirectoryClient(CloudFileShare cloudFileShare)
{
CloudFileShare = cloudFileShare;
}
public CloudFileShare CloudFileShare { get; }
public CloudFileDirectory CloudFileDirectory { get; set; }
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">AzureStorageFileClient</span></h2>
This class provides functions to different File related operations<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public interface IAzureStorageFileClient
{
CloudFileDirectory CloudFileDirectory { get; set; }
CloudFile CloudFile { get; }
}
public class AzureStorageFileClient : IAzureStorageFileClient
{
public AzureStorageFileClient(CloudFile cloudFile)
{
CloudFile = cloudFile;
}
public CloudFileDirectory CloudFileDirectory { get; set; }
public CloudFile CloudFile { get; }
}
</code>
</pre>
<br />
<br />
As you have noted none of the above classes have any functions.<br />
Below is the extension class that provides functions available to those interfaces.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">AzureStorageAccountExtensions</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public static class AzureStorageAccountExtensions
{
public static async Task<iazurestoragefiledirectoryclient> GetOrAddShare(this IAzureStorageShareClient client, string shareName)
{
var cloudFileShare = client.CloudFileClient.GetShareReference(shareName);
await cloudFileShare.CreateIfNotExistsAsync().ConfigureAwait(false);
return new AzureStorageFileDirectoryClient(cloudFileShare);
}
public static async Task<iazurestoragefiledirectoryclient> GetOrAddFolder(this IAzureStorageFileDirectoryClient client, string folderPath)
{
var cloudRootDirectory = client.CloudFileShare.GetRootDirectoryReference();
var cloudFileDirectory = cloudRootDirectory.GetDirectoryReference(folderPath);
await cloudFileDirectory.CreateIfNotExistsAsync().ConfigureAwait(false);
client.CloudFileDirectory = cloudFileDirectory;
return client;
}
public static IEnumerable<ilistfileitem> ListFiles(this IAzureStorageFileDirectoryClient client)
{
return client.CloudFileDirectory.ListFilesAndDirectories().Where(f => !f.Uri.LocalPath.Contains(".folder"));
}
public static async Task<iazurestoragefileclient> AddFile(this IAzureStorageFileDirectoryClient client, string fileName, long length)
{
await client.CloudFileDirectory.CreateIfNotExistsAsync().ConfigureAwait(false);
var cloudFile = client.CloudFileDirectory.GetFileReference(fileName);
if (!await cloudFile.ExistsAsync().ConfigureAwait(false))
{
await cloudFile.CreateAsync(length).ConfigureAwait(false);
}
return new AzureStorageFileClient(cloudFile);
}
public static async Task<byte> GetFile(this IAzureStorageFileDirectoryClient client, string fileName)
{
var cloudFile = client.CloudFileDirectory.GetFileReference(fileName);
if (await cloudFile.ExistsAsync().ConfigureAwait(false))
{
using (MemoryStream ms = new MemoryStream())
{
await cloudFile.DownloadToStreamAsync(ms).ConfigureAwait(false);
return ms.ToArray();
}
}
return null;
}
public static async void UploadFile(this IAzureStorageFileClient client, byte[] data)
{
await client.CloudFile.UploadFromByteArrayAsync(data, 0, data.Length).ConfigureAwait(false);
}
}
</byte></iazurestoragefileclient></ilistfileitem></iazurestoragefiledirectoryclient></iazurestoragefiledirectoryclient></code>
</pre>
<br />
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-64844313750949777472017-10-17T23:24:00.003+11:002017-10-17T23:24:58.341+11:00EPiFind AndAny/OrAny Filter Expression Builder <div dir="ltr" style="text-align: left;" trbidi="on">
Extending the EPiServer Find Functionality to support OrAny or AndAny Filter Expression<br />
<br />
The OrAny and AndAny filter for EPiFind will allow the developer to combine list of contents as And or Or with the other query conditions.<br />
<br />
We had custom property Tag and TagList class which we wanted EPiFind to index so we can query the contents based on those filters.<br />
<br />
The Tag and TagList class looks like below<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">
T</span><span style="color: red;">ag Class</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">[JsonConverter(typeof(TagJsonConverter))]
public class Tag
{
private string text;
private string value;
private string tag;
private bool isLoaded;
public string Text
{
get
{
if (!isLoaded)
Load();
return text;
}
set
{
text = value;
}
}
public string Value
{
get
{
if (!isLoaded)
Load();
return value;
}
set
{
this.value = value;
}
}
public Tag()
{ }
public Tag(string tag)
{
this.tag = tag;
}
public Tag(string value, string text)
{
isLoaded = true;
this.value = value;
this.text = text;
}
public string GetId(string type) => Value?.Substring(type.Length);
public override string ToString()
{
return $"{Value}{Constants.Tags.NameDelimiter}{Text}";
}
private void Load()
{
isLoaded = true;
if (!string.IsNullOrWhiteSpace(tag))
{
var parts = tag.Split(new[] { Constants.Tags.NameDelimiter }, 2);
if (parts.Length == 1)
{
text = parts[0];
value = string.Empty;
}
else
{
value = parts[0];
text = parts[1];
}
}
}
public static Tag Parse(string tag)
{
return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
}
}</code>
</pre>
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">TagList Class</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">[JsonConverter(typeof(TagListJsonConverter))]
public class TagList : IEnumerable<tag>
{
private Tag[] tags;
private string tag;
private bool isLoaded;
public TagList() { }
public TagList(string tag)
{
tags = new Tag[0];
this.tag = tag;
}
public TagList(IEnumerable<tag> tags)
{
isLoaded = true;
this.tags = tags.ToArray();
}
public override string ToString() => string.Join(Constants.Tags.ValueDelimiter.ToString(), this.Select(s => s.ToString()));
public IEnumerator<tag> GetEnumerator()
{
if (!isLoaded)
Load();
return ((IEnumerable<tag>)this.tags).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private void Load()
{
isLoaded = true;
if (!string.IsNullOrWhiteSpace(tag))
tags = tag.Split(new char[] { Constants.Tags.ValueDelimiter }, StringSplitOptions.RemoveEmptyEntries).Select(s => new Tag(s)).ToArray();
}
}</tag></tag></tag></tag></code>
</pre>
<br />
We use the Tag and TagList class like below just for reference so you can get a context<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">[Display(Order = 20, Description = "Start typing the topic name to see suggestions. Allows multiple.")]
[AutoSuggestTags(typeof(TopicSelectionQuery))]
[BackingType(typeof(PropertyTagList))]
public virtual TagList Topics { get; set; }
[BackingType(typeof(PropertyTag))]
[Display(Order = 30, Description = "Select competition to filter the contents of this section block")]
[LinkedAutoSelection(typeof(CompetitionTagSelectionQuery), null, null, new[] { "/season/topic", "/round/topic" })]
public virtual Tag Competition { get; set; }
</code></pre>
<br />
The Problem<br />
<br />
We wanted to filter the contents by applying filter on Tag and TagList class. Therefore, we build a filter expression using the below code for different property.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">OrTopic Filter Builder</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">public static ITypeSearch<t> TopicOrFilter<t>(this ITypeSearch<t> typeSearchQuery, IEnumerable<contentreference> values)
where T : ContentPage
{
if (values.IsNullOrEmpty())
return typeSearchQuery;
FilterBuilder<t> filterBuilder = new FilterBuilder<t>(typeSearchQuery.Client);
foreach (var value in values)
{
filterBuilder = filterBuilder.Or(t => t.Topic.Match(value));
}
return typeSearchQuery.Filter(filterBuilder);
}</t></t></contentreference></t></t></t></code>
</pre>
<br />
<br />
but we wanted this to be generic so we do not have to add separate function for each paramter.<br />
<br />
Therefore we added below generic code<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">The AndAny Function</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">public static ITypeSearch<t> AndAny<t tval="">(this ITypeSearch<t> typeSearchQuery, IEnumerable<tval> values, Expression<func filter="" tval="">> filter)
where T : ContentPage
{
if (values.IsNullOrEmpty())
return typeSearchQuery;
FilterBuilder<t> filterBuilder = new FilterBuilder<t>(typeSearchQuery.Client);
/* Expression visitor to modify provided filter to epiFind filter expression */
var resolver = new ParameterReplacerVisitor<func filter="" tval="">, Func<t filter="">>();
/* Parameter to be replaced with constant */
var parameter = filter.Parameters[1];
foreach (var value in values)
{
resolver.Source = parameter;
resolver.Value = value;
filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
}
return typeSearchQuery.Filter(filterBuilder);
}</t></func></t></t></func></tval></t></t></t></code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">The OrAny Function</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">public static ITypeSearch<t> OrAny<t tval="">(this ITypeSearch<t> typeSearchQuery, IEnumerable<tval> values, Expression<func filter="" tval="">> filter)
where T : ContentPage
{
if (values.IsNullOrEmpty())
return typeSearchQuery;
FilterBuilder<t> filterBuilder = new FilterBuilder<t>(typeSearchQuery.Client);
/* Expression visitor to modify provided filter to epiFind filter expression */
var resolver = new ParameterReplacerVisitor<func filter="" tval="">, Func<t filter="">>();
/* Parameter to be replaced with constant */
var parameter = filter.Parameters[1];
foreach (var value in values)
{
resolver.Source = parameter;
resolver.Value = value;
filterBuilder = filterBuilder.Or(resolver.ReplaceValue(filter));
}
return typeSearchQuery.OrFilter(filterBuilder);
}</t></func></t></t></func></tval></t></t></t></code>
</pre>
<br />
The Other Problem.<br />
<br />
The EPiFind filter exprssion expects fitler expression with one parameter but our expression contains the two parameter. Therefore, we need to resolve parameter expression to align with the functions available for filter builder.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">public class ParameterReplacerVisitor<tin tout=""> : System.Linq.Expressions.ExpressionVisitor
{
public ParameterReplacerVisitor()
{
}
public ParameterExpression Source { get; set; }
public object Value { get; set; }
public Expression<tout> ReplaceValue(Expression<tin> exp)
{
var expNew = Visit(exp) as LambdaExpression;
return Expression.Lambda<tout>(expNew.Body, expNew.Parameters);
}
/// <summary>
/// This functoin will replace the parameter expression with constant values if parameter name and type matches with provided value
/// for e.g (num1, num2) => num1 + num2 will be replaced (num1) => num1 + 3. if we want to replace num2 with 3.
/// </summary>
protected override Expression VisitParameter(ParameterExpression node)
{
// Replace the source with the target, visit other params as usual.
if (node.Type == Source.Type && node.Name == Source.Name)
{
return Expression.Constant(Value);
}
return base.VisitParameter(node);
}
/// <summary>
/// This functoin will replace the member expression with constant values if parameter name and type matches with provided value
/// for e.g (num1, classObj) => num1 + classObj.num2 will be replaced (num1) => num1 + 3. if we want to replace classObj.num2 with 3.
/// </summary>
protected override Expression VisitMember(MemberExpression m)
{
if (m.Expression != null
&& m.Expression.NodeType == ExpressionType.Parameter
&& m.Expression.Type == Source.Type && ((ParameterExpression)m.Expression).Name == Source.Name)
{
object newVal;
if (m.Member is FieldInfo)
newVal = ((FieldInfo)m.Member).GetValue(Value);
else if (m.Member is PropertyInfo)
newVal = ((PropertyInfo)m.Member).GetValue(Value, null);
else
newVal = null;
return Expression.Constant(newVal);
}
return base.VisitMember(m);
}
/// <summary>
/// In this function we are reducing the number of parameters in lambda expression
/// for e.g as we are replacing parameter with constants therefore, (num1, num2) => will become (num1) =>
/// </summary>
protected override Expression VisitLambda<t>(Expression<t> node)
{
var parameters = node.Parameters.Where(p => p.Name != Source.Name || p.Type != Source.Type).ToList();
return Expression.Lambda(Visit(node.Body), parameters);
}
}</t></t></tout></tin></tout></tin></code>
</pre>
<br />
<br />
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-30315050481695254502017-10-09T15:43:00.001+11:002017-10-09T15:47:17.306+11:00Custom Property In EPiServer And EPiFInd Indexing<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: left;">
In one of our project we had to build the autosuggest property. However, the default type of autosuggest property is string but we wanted to store custom object so we can index through EPiFind with additional values.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Therefore, we implemented custom class to store additional values and also needed JsonConverter for 2 reasons</div>
<div style="text-align: left;">
<br /></div>
<ol style="text-align: left;">
<li>Convert Class to String for Dojo Framework so existing autosuggest property works as expected</li>
<li>Convert and index object for EPiFind differently</li>
</ol>
<div>
We will delimte the value of autosuggest property with '-' to break it into multiple property and save it and index it properly.</div>
<div>
<br /></div>
<div>
<h2 style="text-align: left;">
<span style="color: red;">Define the property</span></h2>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[BackingType(typeof(PropertyTag))]
[Display(Order = 45, Description = "Select competition to filter the contents of the page")]
[AutoSuggestSelection(typeof(TagSelectionQuery), AllowCustomValues = false)]
public virtual Tag Competition { get; set; }
</code></pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Define the PropertyTag Class</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[PropertyDefinitionTypePlugIn(Description = "A property to tag content", DisplayName = "Tag")]
[JsonConverter(typeof(TagJsonConverter))]
public class PropertyTag : PropertyLongString
{
public override PropertyDataType Type => PropertyDataType.LongString;
public override Type PropertyValueType => typeof(Tag);
public override object Value
{
get { return Tag.Parse(base.Value?.ToString()); }
set { base.Value = value?.ToString(); }
}
public override object SaveData(PropertyDataCollection properties)
{
return this.LongString;
}
}
</code></pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Define the JsonConverter</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
internal class TagJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value is Tag tag)
return tag;
return new Tag(reader.Value.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
if (writer is MaxDepthJsonWriter && value is Tag tag)
{
writer.WriteStartObject();
writer.WritePropertyName($"Text{TypeSuffix.String}");
writer.WriteValue(tag.Text);
writer.WritePropertyName($"Value{TypeSuffix.String}");
writer.WriteValue(tag.Value);
writer.WriteEndObject();
return;
}
if (value is Tag itag)
{
writer.WriteValue(itag.ToString());
}
else
{
writer.WriteValue(value.ToString());
}
}
}
</code></pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Define Tag Class</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[JsonConverter(typeof(TagJsonConverter))]
public class Tag
{
private string text;
private string value;
private string tag;
private bool isLoaded;
public string Text
{
get
{
if (!isLoaded)
Load();
return text;
}
set
{
text = value;
}
}
public string Value
{
get
{
if (!isLoaded)
Load();
return value;
}
set
{
this.value = value;
}
}
public Tag()
{ }
public Tag(string tag)
{
this.tag = tag;
}
public Tag(string value, string text)
{
isLoaded = true;
this.value = value;
this.text = text;
}
public string GetId(string type) => Value?.Substring(type.Length);
public override string ToString()
{
return $"{Value}{ "-" }{Text}";
}
private void Load()
{
isLoaded = true;
if (!string.IsNullOrWhiteSpace(tag))
{
var parts = tag.Split(new[] { "-" }, 2);
if (parts.Length == 1)
{
text = parts[0];
value = string.Empty;
}
else
{
value = parts[0];
text = parts[1];
}
}
}
public static Tag Parse(string tag)
{
return string.IsNullOrWhiteSpace(tag) ? null : new Tag(tag);
}
}
</code></pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Define TagSelectionQuery</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(ISelectionQuery))]
public class TagSelectionQuery : ISelectionQuery
{
private const int Depth = 0;
protected virtual string Prefix { get; } = Constants.Tags.Competition;
public IEnumerable<iselectitem> GetItems(string query)
{
/* Return List Of Select Items With Value Delimited by '-' For value and text of tag
}
/// <summary> /// Will be called when initializing an editor with an existing value to get the corresponding text representation.
/// </summary> public ISelectItem GetItemByValue(string value) => /* Return SelectItem */;<icontent><competitionpage><t bool=""><t>
}
</t></t></competitionpage></icontent></iselectitem></code></pre>
<br />
<br />
<b><span style="color: red;">Pro Tip</span></b>: Make Sure Admin For Property Looks Like below<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0q87BAZYHjSeYRJJPjqnNgKOpk1Unz0-IyHDXJ3ORWYHOX5rtc_nfm6Dgswau1oxg2hqLc6CFP2tdoTVPiubMv-NsYn2pKNrSe1VOARFSm0MbltqDX_HPyFztobSuM7eLVqMmQA9sLJM/s1600/BackendType.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="124" data-original-width="804" height="96" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0q87BAZYHjSeYRJJPjqnNgKOpk1Unz0-IyHDXJ3ORWYHOX5rtc_nfm6Dgswau1oxg2hqLc6CFP2tdoTVPiubMv-NsYn2pKNrSe1VOARFSm0MbltqDX_HPyFztobSuM7eLVqMmQA9sLJM/s640/BackendType.png" width="640" /></a></div>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-7498774289848379712017-08-29T23:32:00.000+10:002017-08-29T23:32:04.543+10:00Twitter And Instagram Content Provider for EPiServer<div dir="ltr" style="text-align: left;" trbidi="on">
In our project we had to bring the twitter and instagram content as content in EPiServer so editor can dragged twitter and instagram into content area.<br />
<br />
Firstly, Our company uses spredfast to manage the social content. therefore, we had to use spredfast api to fetch the twitter and instagram content and show it as content in EPiServer content provider.<br />
<br />
I am not going to show the spredfast api part. We will assume we are fetching twitter and instagram through api and saving it in our local database as json. We will retrieve this json coming from our local database and convert that json c# classes to EPiServer contetn classes and serve it to EPiServer content provider.<br />
<br />
It is how it looks like<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxgimPaVviDw0NeRID7GG1X9QDeGn0r0s626rdHfPxuy_lVuvXUzPlqVdn2xFBVF8h9mr8bzoDl2LwQEbJz3dn0Gx1zo1iRqFpt9kAuaD38lW1nAb4VJzwCdetx9AjorjuvOifXa6_MFg/s1600/Instagram.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="480" data-original-width="482" height="318" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjxgimPaVviDw0NeRID7GG1X9QDeGn0r0s626rdHfPxuy_lVuvXUzPlqVdn2xFBVF8h9mr8bzoDl2LwQEbJz3dn0Gx1zo1iRqFpt9kAuaD38lW1nAb4VJzwCdetx9AjorjuvOifXa6_MFg/s320/Instagram.JPG" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiL2IuNM5pEi-MPaH5ExtExIIU2yFY8xRMdrnBowD186ecGztC_RExndkKKeudPB_uzVZdw7PGzr67B4HCcRCLbO2QDovQIbivDLvThpWv8EIotSuesWSTRCiiYP8pKYfuXPlZN-2F4Q1M/s1600/Twitter.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="476" data-original-width="614" height="248" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiL2IuNM5pEi-MPaH5ExtExIIU2yFY8xRMdrnBowD186ecGztC_RExndkKKeudPB_uzVZdw7PGzr67B4HCcRCLbO2QDovQIbivDLvThpWv8EIotSuesWSTRCiiYP8pKYfuXPlZN-2F4Q1M/s320/Twitter.JPG" width="320" /></a></div>
<br />
<br />
This is how it looks like after dragging four Social Content in Content Area<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1EM8r-KZxs3rJHTeWNxt3p310wKhiqwwxB0pUYZL9EezLk1CTwmA9cN8_E3r3kOOgSI49lSbqwzeaQ-gJdBLHMim7lspVQCNu23sJ8lSl0qGOQQe7acG8WiBtffszdZIthKN3l09hTWM/s1600/Social+View.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="793" data-original-width="658" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1EM8r-KZxs3rJHTeWNxt3p310wKhiqwwxB0pUYZL9EezLk1CTwmA9cN8_E3r3kOOgSI49lSbqwzeaQ-gJdBLHMim7lspVQCNu23sJ8lSl0qGOQQe7acG8WiBtffszdZIthKN3l09hTWM/s320/Social+View.JPG" width="265" /></a></div>
<br />
<br />
<b><span style="color: red;">Parent class for Social Post For Json</span></b><br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="word-wrap: normal;">
<code style="color: black; word-wrap: normal;">
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NRL.Shared.Models.Social
{
public abstract class SocialPost
{
public string Entity_id { get; set; }
public object Id { get; set; }
/// <summary>
/// Gets Entity Id for API Callling. For Instagram Spredfast API accepts entity_id for twitter it accepts id
/// </summary>
public abstract string ApiEntityId { get; }
public abstract DateTimeOffset Created { get; set; }
public abstract SocialType SocialType { get; }
public abstract string AvailableText { get; }
}
public enum SocialType
{
Twitter,
Instagram,
}
}
</code></code></pre>
<br />
<br />
<b><span style="color: red;">Below is the trimmed version of twitter json and c# class</span></b><br />
<b><span style="color: red;"><br /></span></b>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="word-wrap: normal;">
<code style="color: black; word-wrap: normal;">
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Globalization;
namespace NRL.Shared.Models.Social.Twitter
{
public class TwitterPost : SocialPost
{
private DateTimeOffset? createdDate = null;
public override string ApiEntityId => Id_str;
public override SocialType SocialType => SocialType.Twitter;
public override DateTimeOffset Created
{
get
{
if (!createdDate.HasValue)
{
return DateTimeOffset.ParseExact(Created_at, "ddd MMM dd HH:mm:ss K yyyy", new CultureInfo("en-AU"));
}
return createdDate.Value;
}
set
{
createdDate = value;
}
}
public override string AvailableText
{
get
{
return string.IsNullOrWhiteSpace(Full_Text) ? Text ?? string.Empty : Full_Text;
}
}
public string Created_at { get; set; }
public string Id_str { get; set; }
public string Full_Text { get; set; }
public string Text { get; set; }
public bool Truncated { get; set; }
public Entities Entities { get; set; }
public User User { get; set; }
}
public class Medium
{
public string Media_url { get; set; }
}
public class User
{
public string Screen_name { get; set; }
}
public class Entities
{
public Medium[] Media { get; set; }
}
}
</code></code></pre>
<br />
<br />
<b><span style="color: red;">Below is the trimmed version of instagram json and c# class</span></b><br />
<b><span style="color: red;"><br /></span></b>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NRL.Shared.Models.Social.Instagram
{
public class InstagramPost : SocialPost
{
private DateTimeOffset? createdDate = null;
public User User { get; set; }
public Images Images { get; set; }
public string Link { get; set; }
public override string ApiEntityId => Entity_id;
public override SocialType SocialType => SocialType.Instagram;
public override DateTimeOffset Created
{
get
{
if (!createdDate.HasValue)
{
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(Created_Time));
}
return createdDate.Value;
}
set
{
createdDate = value;
}
}
public override string AvailableText
{
get
{
return string.IsNullOrWhiteSpace(Caption?.Text) ? Caption.Text ?? string.Empty : Caption.Text;
}
}
public string Created_Time { get; set; }
public Caption Caption { get; set; }
}
public class User
{
public string Username { get; set; }
}
public class Image
{
public string Url { get; set; }
}
public class Images
{
public Image Standard_Resolution { get; set; }
}
public class Caption
{
public string Text { get; set; }
}
}
</code></pre>
<br />
<br />
We would now need twitter and instagram content class for EPiServer. EPiServer will need ContentFolder and Content Class to show in edit section therefore, we would need to convert TwitterPost and InstagramPost class to Twitter and Instagram Content Class.<br />
<br />
<b><span style="color: red;">SocialContent Base Class</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">using EPiServer.Core;
using EPiServer.DataAnnotations;
using EPiServer.Shell.ObjectEditing;
using NRL.Web.Business.Models.Pages;
using NRL.Web.Business.Selection;
using System.ComponentModel.DataAnnotations;
using System;
using NRL.Shared.Models.Social;
namespace NRL.Web.Business.Models.Social
{
[ContentType(GUID = "B2E4619D-9826-4D07-ADF7-9949E89CA6B8")]
public abstract class SocialContent : ContentBase
{
[Display(Name= "Social Type")]
[SelectOne(SelectionFactoryType = typeof(EnumSelectionFactory<SocialType>))]
public virtual SocialType SocialType { get; set; }
[Display(Name = "Id")]
public virtual string Id { get; set; }
public abstract ContentPageType ContentPageType { get; }
public abstract DateTime Published { get; }
public abstract string GetImageUrl();
public abstract int GetLength();
public abstract string GetSummary();
public abstract string GetTopic();
}
public enum ContentType
{
ContentFolder,
DateContentFolder,
TwitterContent,
InstagramContent,
}
}
</code></pre>
<br />
<b><span style="color: red;"><br /></span></b>
<b><span style="color: red;">TwitterContent Class</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ContentType(GUID = "E4B96F8A-7DFF-41E0-B42B-D4C36337DD80", DisplayName = "Twitter Content", AvailableInEditMode = false)]
public class TwitterContent : SocialContent
{
[Ignore]
public TwitterPost Post { get; set; }
public override ContentPageType ContentPageType => ContentPageType.Social;
public override DateTime Published => Post.Created.DateTime;
public override string GetImageUrl()
{
var image = Post.Entities?.Media?.FirstOrDefault();
return Business.Helpers.UrlHelper.ImageUrl(image?.Media_url);
}
public override int GetLength()
{
return Post.AvailableText?.Length ?? 0;
}
public override string GetSummary()
{
return Post.AvailableText?.SafeSubString(0, 100);
}
public override string GetTopic()
{
return "Twitter";
}
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">InstagramContent Class</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ContentType(GUID = "0B25DF14-285F-4647-B1B5-262E952C1781", DisplayName = "Instagram Content", AvailableInEditMode = false)]
public class InstagramContent : SocialContent
{
[Ignore]
public InstagramPost Post { get; set; }
public override ContentPageType ContentPageType => ContentPageType.Social;
public override DateTime Published => Post.Created.DateTime;
public override string GetImageUrl()
{
var image = Post.Images?.Standard_Resolution?.Url;
return Business.Helpers.UrlHelper.ImageUrl(image);
}
public override int GetLength()
{
return Post.Caption?.Text?.Length ?? 0;
}
public override string GetSummary()
{
return Post.Caption?.Text?.SafeSubString(0, 100);
}
public override string GetTopic()
{
return "Instagram";
}
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">Social FolderContent Class</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ContentType(GUID = "DE8A22EF-EDD4-4F42-BD8B-4DEFFE3C8244", DisplayName = "Spredfast Folder", AvailableInEditMode = false, GroupName = "Social")]
public class SocialContentFolder : ContentFolder
{
[Ignore]
public SocialType SocialType { get; set; }
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">Social Content Provider Class</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public sealed class SocialContentProvider : ContentProvider
{
private static readonly object Padlock = new object();
private static SocialContentProvider instance = null;
private IdentityMappingService identityMappingService;
private IContentTypeRepository contentTypeRepository;
private IContentFactory contentFactory;
public const string DateFolderFormat = "dd-MM-yyyy";
public const string Key = "social";
public const string InstagramKey = "Instagram_Streams";
public const string TwitterKey = "Twitter_Streams";
private SocialContentProvider()
{
identityMappingService = ServiceLocator.Current.GetInstance<IdentityMappingService>();
contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
contentFactory = ServiceLocator.Current.GetInstance<IContentFactory>();
/* Create provider root if not exists
* Provider root will be different for each site so that they can be cached and view seperately
* Add configuration settings for entry point and capabilites */
var socialRoot = GetEntryPoint();
var providerValues = new NameValueCollection();
providerValues.Add(ContentProviderElement.EntryPointString, socialRoot.ContentLink.ToString());
providerValues.Add(ContentProviderElement.CapabilitiesString, "Create, Edit, Delete, Search");
/* initialize and register the provider */
Initialize(Key, providerValues);
}
public static SocialContentProvider Instance
{
get
{
lock (Padlock)
{
if (instance == null)
{
instance = new SocialContentProvider();
}
return instance;
}
}
}
public SiteDefinition CurrentDefinition
{
get
{
return SiteDefinition.Current;
}
}
public List<InstagramPost> InstagramFeeds
{
get
{
// Load Instagram Json and convert to instagram post
}
}
public List<TwitterPost> TwitterPosts
{
get
{
// Load Twitter Json and convert to twiter post
}
}
protected override void SetCacheSettings(ContentReference contentReference, IEnumerable<GetChildrenReferenceResult> children, CacheSettings cacheSettings)
{
// Set a low cache setting so new items are fetched from data source, but keep the
// items already fetched for a long time in the cache.
cacheSettings.SlidingExpiration = System.Web.Caching.Cache.NoSlidingExpiration;
cacheSettings.AbsoluteExpiration = DateTime.Now.AddMinutes(5);
base.SetCacheSettings(contentReference, children, cacheSettings);
}
protected override ContentResolveResult ResolveContent(ContentReference contentLink)
{
var mappedItem = identityMappingService.Get(contentLink);
if (mappedItem == null)
return null;
return ResolveContent(mappedItem);
}
protected override ContentResolveResult ResolveContent(Guid contentGuid)
{
var mappedItem = identityMappingService.Get(contentGuid);
if (mappedItem == null)
return null;
return ResolveContent(mappedItem);
}
private IContent LoadContent(MappedIdentity mappedIdentity, ILanguageSelector languageSelector)
{
ContentType socialContentType;
SocialType socialType;
SetSocialContentType(mappedIdentity.ExternalIdentifier, out socialContentType, out socialType);
string entityId = null;
switch (socialContentType)
{
case ContentType.DateContentFolder:
return CreateFolder(mappedIdentity, socialContentType, mappedIdentity.ExternalIdentifier.Segments[4]);
case ContentType.ContentFolder:
return CreateFolder(mappedIdentity, socialContentType, socialType.ToString());
case ContentType.TwitterContent:
{
/* Get twitter entity id */
entityId = mappedIdentity.ExternalIdentifier.Segments[4];
var twitterPost = TwitterPosts.FirstOrDefault(t => t.ApiEntityId.Equals(entityId));
return CreateTwitter(mappedIdentity, socialContentType, twitterPost);
}
case ContentType.InstagramContent:
{
/* Get instagram entity id */
entityId = mappedIdentity.ExternalIdentifier.Segments[4];
var instagramPost = InstagramFeeds.FirstOrDefault(t => t.ApiEntityId.Equals(entityId));
return CreateInstagram(mappedIdentity, socialContentType, instagramPost);
}
}
return null;
}
protected override IContent LoadContent(ContentReference contentLink, ILanguageSelector languageSelector)
{
MappedIdentity mappedItem = identityMappingService.Get(contentLink);
if (mappedItem == null)
return null;
return LoadContent(mappedItem, languageSelector);
}
protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes(ContentReference contentLink, string languageID, out bool languageSpecific)
{
languageSpecific = false;
var children = new List<GetChildrenReferenceResult>();
/* If user is expanding root folder then show tiwtter and instagram folders */
if (EntryPoint.CompareToIgnoreWorkID(contentLink))
{
children.Add(new GetChildrenReferenceResult { ContentLink = identityMappingService.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Twitter.ToString()}/{ContentType.ContentFolder.ToString()}/{contentLink.ID}/{CurrentDefinition.Id}/"), true).ContentLink, IsLeafNode = false, ModelType = typeof(SocialContentFolder) });
children.Add(new GetChildrenReferenceResult { ContentLink = identityMappingService.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Instagram.ToString()}/{ContentType.ContentFolder.ToString()}/{contentLink.ID}/{CurrentDefinition.Id}/"), true).ContentLink, IsLeafNode = false, ModelType = typeof(SocialContentFolder) });
return children;
}
var mappedItem = identityMappingService.Get(contentLink);
if (mappedItem == null)
return children;
ContentType socialContentType;
SocialType socialType;
SetSocialContentType(mappedItem.ExternalIdentifier, out socialContentType, out socialType);
switch (socialType)
{
case SocialType.Instagram:
children.AddRange(GetInstagramChildReference(socialContentType, contentLink, mappedItem));
break;
case SocialType.Twitter:
children.AddRange(GetTwitterChildReference(socialContentType, contentLink, mappedItem));
break;
}
return children;
}
public static ContentFolder GetEntryPoint()
{
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var folder = contentRepository.GetBySegment(ContentReference.RootPage, Key, LanguageSelector.AutoDetect()) as ContentFolder;
if (folder == null)
{
folder = contentRepository.GetDefault<ContentFolder>(ContentReference.RootPage);
folder.Name = Key;
contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess);
}
return folder;
}
private IEnumerable<GetChildrenReferenceResult> GetInstagramChildReference(ContentType socialContentType, ContentReference parentContentLink, MappedIdentity mappedItem)
{
var children = new List<GetChildrenReferenceResult>();
var instagramStreams = InstagramFeeds.GroupBy(g => g.Created.ToTimeZone(StateTimeZones.Nsw).Date).Select(s => new
{
Date = s.Key,
Feeds = s.ToList<InstagramPost>()
});
switch (socialContentType)
{
case ContentType.DateContentFolder:
{
var dateGroupedInstagrams = instagramStreams.FirstOrDefault(i => i.Date.ToString(DateFolderFormat).Equals(mappedItem.ExternalIdentifier.Segments[4]))?.Feeds;
if (dateGroupedInstagrams != null)
{
children.AddRange(GetChildren(
dateGroupedInstagrams,
(InstagramPost i) => MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Instagram.ToString()}/{ContentType.InstagramContent.ToString()}/{parentContentLink.ID}/{i.ApiEntityId.ToString()}"),
typeof(InstagramContent),
true));
}
}
break;
case ContentType.ContentFolder:
{
children.AddRange(GetChildren(
instagramStreams.Select(s => s.Date).ToList<DateTime>(),
(DateTime date) => MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Instagram.ToString()}/{ContentType.DateContentFolder.ToString()}/{parentContentLink.ID}/{date.ToString(DateFolderFormat)}"),
typeof(SocialContentFolder)));
}
break;
}
return children;
}
private IEnumerable<GetChildrenReferenceResult> GetTwitterChildReference(ContentType socialContentType, ContentReference parentContentLink, MappedIdentity mappedItem)
{
var children = new List<GetChildrenReferenceResult>();
var twitterPosts = TwitterPosts.GroupBy(g => g.Created.ToTimeZone(StateTimeZones.Nsw).Date).Select(s => new
{
Date = s.Key,
Posts = s.ToList<TwitterPost>()
});
switch (socialContentType)
{
case ContentType.DateContentFolder:
{
var dateGroupedTwitter = twitterPosts.FirstOrDefault(i => i.Date.ToString(DateFolderFormat).Equals(mappedItem.ExternalIdentifier.Segments[4]))?.Posts;
if (dateGroupedTwitter != null)
{
children.AddRange(GetChildren(
dateGroupedTwitter,
(TwitterPost i) => MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Twitter.ToString()}/{ContentType.TwitterContent.ToString()}/{parentContentLink.ID}/{i.ApiEntityId.ToString()}"),
typeof(TwitterContent),
true));
}
}
break;
case ContentType.ContentFolder:
{
children.AddRange(GetChildren(
twitterPosts.Select(s => s.Date).ToList<DateTime>(),
(DateTime date) => MappedIdentity.ConstructExternalIdentifier(ProviderKey, $"{SocialType.Twitter.ToString()}/{ContentType.DateContentFolder.ToString()}/{parentContentLink.ID}/{date.ToString(DateFolderFormat)}"),
typeof(SocialContentFolder)));
}
break;
}
return children;
}
private IEnumerable<GetChildrenReferenceResult> GetChildren<T>(List<T> inputList, Func<T, Uri> createUniqueUri, Type modelType, bool isLeafNode = false)
{
var children = new List<GetChildrenReferenceResult>();
foreach (var t in inputList)
{
var externalIdentifier = identityMappingService.Get(createUniqueUri(t), true);
children.Add(new GetChildrenReferenceResult
{
ContentLink = externalIdentifier.ContentLink,
IsLeafNode = isLeafNode,
ModelType = modelType
});
}
return children;
}
private void SetSocialContentType(Uri externalIdentifier, out ContentType socialContentType, out SocialType socialType)
{
/* Getting social type */
socialType = (SocialType)Enum.Parse(typeof(SocialType), RemoveEndingSlash(externalIdentifier.Segments[1]), true);
/* Getting social content type */
socialContentType = (ContentType)Enum.Parse(typeof(ContentType), RemoveEndingSlash(externalIdentifier.Segments[2]), true);
}
private IContent CreateContent(MappedIdentity mappedIdentity, ContentType contentType, Type modelType, string name, DateTime createDateTime)
{
return CreateContent(mappedIdentity.ContentLink.ID, mappedIdentity.ContentGuid, int.Parse(RemoveEndingSlash(mappedIdentity.ExternalIdentifier.Segments[3])), contentType, modelType, name, createDateTime);
}
private IContent CreateContent(int contentId, Guid contentGuid, int parentContentId, ContentType contentType, Type modelType, string name, DateTime createDateTime)
{
/* Find parent */
var parentLink = EntryPoint;
/* Getting parent id */
if (contentType != ContentType.ContentFolder)
parentLink = new ContentReference(parentContentId, ProviderKey);
EPiServer.DataAbstraction.ContentType epiContentType = contentTypeRepository.Load(modelType);
var content = contentFactory.CreateContent(epiContentType);
content.ContentTypeID = epiContentType.ID;
content.ParentLink = parentLink;
content.ContentGuid = contentGuid;
content.ContentLink = new ContentReference(contentId, ProviderKey);
content.Name = name;
var securable = content as IContentSecurable;
securable.GetContentSecurityDescriptor().AddEntry(new AccessControlEntry(EveryoneRole.RoleName, AccessLevel.Read));
var versionable = content as IVersionable;
if (versionable != null)
{
versionable.Status = VersionStatus.Published;
versionable.IsPendingPublish = false;
versionable.StartPublish = createDateTime.AddDays(-1);
}
var changeTrackable = content as IChangeTrackable;
if (changeTrackable != null)
{
changeTrackable.Created = createDateTime;
changeTrackable.Changed = createDateTime;
changeTrackable.Saved = createDateTime;
}
return content;
}
private string RemoveEndingSlash(string virtualPath)
{
return !string.IsNullOrEmpty(virtualPath) && virtualPath[virtualPath.Length - 1] == '/' ? virtualPath.Substring(0, virtualPath.Length - 1) : virtualPath;
}
private SocialContentFolder CreateFolder(MappedIdentity mappedIdentiy, ContentType contentType, string name)
{
var content = CreateContent(mappedIdentiy, contentType, typeof(SocialContentFolder), name, DateTime.Now.ToStatesDateTime(StateTimeZones.Nsw.ToString())) as SocialContentFolder;
return content;
}
private InstagramContent CreateInstagram(int contentId, Guid contentGuid, int parentContentId, ContentType contentType, InstagramPost instagramPost)
{
var name = $"@{instagramPost.User.Username}: {instagramPost.Caption?.Text?.SafeSubString(0, 200)}";
var content = CreateContent(contentId, contentGuid, parentContentId, contentType, typeof(InstagramContent), name, instagramPost.Created.ToTimeZone(StateTimeZones.Nsw).DateTime) as InstagramContent;
content.Id = instagramPost.ApiEntityId.ToString();
content.SocialType = SocialType.Instagram;
content.Post = instagramPost;
return content;
}
private InstagramContent CreateInstagram(MappedIdentity mappedIdentiy, ContentType contentType, InstagramPost instagramPost)
{
return CreateInstagram(mappedIdentiy.ContentLink.ID, mappedIdentiy.ContentGuid, int.Parse(RemoveEndingSlash(mappedIdentiy.ExternalIdentifier.Segments[3])), contentType, instagramPost);
}
private TwitterContent CreateTwitter(int contentId, Guid contentGuid, int parentContentId, ContentType contentType, TwitterPost twitterPost)
{
var name = $"@{twitterPost.User.Screen_name}: {twitterPost.AvailableText.SafeSubString(0, 200)}";
var content = CreateContent(contentId, contentGuid, parentContentId, contentType, typeof(TwitterContent), name, twitterPost.Created.ToTimeZone(StateTimeZones.Nsw).DateTime) as TwitterContent;
content.Id = twitterPost.ApiEntityId.ToString();
content.SocialType = SocialType.Twitter;
content.Post = twitterPost;
return content;
}
private TwitterContent CreateTwitter(MappedIdentity mappedIdentiy, ContentType contentType, TwitterPost twitterPost)
{
return CreateTwitter(mappedIdentiy.ContentLink.ID, mappedIdentiy.ContentGuid, int.Parse(RemoveEndingSlash(mappedIdentiy.ExternalIdentifier.Segments[3])), contentType, twitterPost);
}
private ContentResolveResult ResolveContent(MappedIdentity mappedItem)
{
ContentType socialContentType;
SocialType socialType;
SetSocialContentType(mappedItem.ExternalIdentifier, out socialContentType, out socialType);
Type type = null;
switch (socialContentType)
{
case ContentType.DateContentFolder:
case ContentType.ContentFolder:
type = typeof(SocialContentFolder);
break;
case ContentType.TwitterContent:
type = typeof(TwitterContent);
break;
case ContentType.InstagramContent:
type = typeof(InstagramContent);
break;
}
var contentRefernce = new ContentReference(mappedItem.ContentLink.ID, ProviderKey);
var contentType = contentTypeRepository.Load(type);
return new ContentResolveResult()
{
ContentLink = contentRefernce,
UniqueID = mappedItem.ContentGuid,
ContentUri = ConstructContentUri(contentType.ID, contentRefernce, mappedItem.ContentGuid)
};
}
}
</code>
</pre>
<br />
<b><span style="color: red;"><br /></span></b>
<b><span style="color: red;">Initializing Content Provider</span></b><br />
Registering SocialContentProvider with EPiServer<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class ContentProviderConfig : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
/* Register provider to episerver this can be done through config as well */
var providerManager = context.Locate.Advanced.GetInstance<IContentProviderManager>();
providerManager.ProviderMap.AddProvider(SocialContentProvider.Instance);
}
public void Uninitialize(InitializationEngine context) { }
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">Social Repository Descriptor</span></b><br />
This class is social repository descriptor which tells the social component what is the key and root of the component and its contained and creatable types.<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(IContentRepositoryDescriptor))]
public class SocialRepositoryDescriptor : ContentRepositoryDescriptorBase
{
protected Injected<IContentProviderManager> ContentProviderManager { get; set; }
public override string Key => SocialContentProvider.Key;
public override string Name => SocialContentProvider.Key;
public override IEnumerable<ContentReference> Roots { get { return new List<ContentReference> { ContentProviderManager.Service.GetProvider(SocialContentProvider.Key).EntryPoint }; } }
public override IEnumerable<Type> ContainedTypes { get { return new[] { typeof(TwitterContent), typeof(InstagramContent) }; } }
public override IEnumerable<Type> MainNavigationTypes { get { return new[] { typeof(ContentFolder), typeof(SocialContentFolder) }; } }
public override IEnumerable<Type> CreatableTypes { get { return new[] { typeof(TwitterContent), typeof(InstagramContent) }; } }
public bool ChangeContextOnItemSelection { get { return true; } }
}
</code>
</pre>
<br />
<b>ChangeContextOnItemSelection </b>allows EPiServer to load content item preview on click<br />
<br />
<span style="color: red;"><b>Social Component Registering</b></span><br />
This class registers the Social Component in asset panel and after this Social Tab will be available in asset pane.<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[Component]
public class SocialComponent : ComponentDefinitionBase
{
public SocialComponent()
: base("epi-cms/widget/HierarchicalList")
{
Categories = new string[] { "content" };
Title = "Social";
Description = "All the social for the given site";
SortOrder = 1000;
PlugInAreas = new[] { PlugInArea.Assets };
Settings.Add(new Setting("repositoryKey", SocialContentProvider.Key));
}
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">Template Descriptor</span></b><br />
We can reuse alloy preview controller to preview the Twitter and Instagram content<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[TemplateDescriptor(
Inherited = true,
TemplateTypeCategory = TemplateTypeCategories.MvcController,
AvailableWithoutTag = false,
ModelType = typeof(TwitterContent),
Tags = new[] { RenderingTags.Edit, RenderingTags.Preview })]
[TemplateDescriptor(
Inherited = true,
TemplateTypeCategory = TemplateTypeCategories.MvcController,
AvailableWithoutTag = false,
ModelType = typeof(InstagramContent),
Tags = new[] { RenderingTags.Edit, RenderingTags.Preview })]
public class SocialCardPreviewController : ActionControllerBase
{
public ActionResult Index(SocialContent currentContent)
{
var model = new NewsPageViewModel(currentContent));
return View(model);
}
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">TblMappedIdentity table</span></b><br />
This part is tricky as we need to convert our social feed into EPiServer content. We can create unique url and then map that unique url to int id through above mentioned table. So when EPiServer comes back with content link or content link id we can fetch back the unique url and fetch back our social details.<br />
<br />
<b><span style="color: red;">Couple of key notes</span></b><br />
In our project we had multiple sites where we wanted to show different folder structure and different content for each sites. However, the content provider is intializes once in lifetime so we can only set one root folder. Having said that we would need to create the sub folder based on the site guid so we can distinguish between different site and fetch content based on current site.<br />
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com1tag:blogger.com,1999:blog-8525160902548525190.post-72255512941875981322017-08-29T22:45:00.000+10:002017-09-05T12:18:18.591+10:00View Trash Content Specific To Site For User in EPiServer<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our project we had very strict access rights policy for EPiServer projects.<br />
We had about 16 sites and each of the sites had editor which could not see the content of other sites.<br />
<br />
We had proper access rights setup on site hierarchy but Recycle Bin EPiServer sits inside the Root Page. The root page and recycle bin had access to everyone therefore, editors could see the deleted content of other editors. Therefore, we had to come up with some approach to restrict the content in recycle bin to editor of that specific site.<br />
<br />
What we decided to create virtual role and make sure content is linked to logged in user site.<br />
As the actual content is physically moved underneath the recycle bin. Therefore, there was no way to find the actual site information. However, before deleting content and moving it to under recycle bin EPiServer saves the actual parent deleted content in Dynamic Data Store named <span style="color: red;"><b>ParentRestoreEntity </b> </span>and can use <b><span style="color: red;">IParentRestoreRepository </span></b>to fetch parent link. You can check the view <b><span style="color: red;">dbo.VW_EPiParentRestoreStore</span></b> on database as well.<br />
<br />
<br />
<b>In order to figure out whether, user has access to deleted content. we will fetch the original parent of deleted content and make sure logged in user has access to that parent content validating that the current logged in user has access to this deleted content as well.</b><br />
<b><span style="color: red;"><br /></span></b>
<b><span style="color: red;">Below is the Virtual Role that we created</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
namespace NRL.Web.Business.VirtualRoles
{
using System.Security.Principal;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Security;
using EPiServer.Web;
using NRL.Shared.Extensions;
using NRL.Web.Business.Extensions;
using NRL.Web.Business.Models.Pages;
using EPiServer.Filters;
public class SiteTrashRole : VirtualRoleProviderBase
{
private IVirtualRoleRepository _repository;
private IParentRestoreRepository _parentRestoreRepository;
private ISiteDefinitionResolver _siteDefinitionResolver;
private const string DefaultRoleName = "SiteTrashRole";
private static string _roleName;
public override string Name
{
get
{
return base.Name ?? "SiteTrashRole";
}
set
{
base.Name = value;
}
}
/// <summary>Gets or sets gets the name of the role.</summary>
/// <value>The name of the role.</value>
public static string RoleName
{
get
{
return SiteTrashRole._roleName ?? (SiteTrashRole._roleName = "SiteTrashRole");
}
set
{
SiteTrashRole._roleName = value;
}
}
/// <summary>
/// Creates a new instance of <see cref="T:EPiServer.Security.CreatorRole" /></summary>
public SiteTrashRole(IVirtualRoleRepository repository, IParentRestoreRepository parentRestoreRepository, ISiteDefinitionResolver siteDefinitionResolver)
{
this.EnableIsInRoleCache = false;
this.SupportsClaims = false;
this._repository = repository;
this._parentRestoreRepository = parentRestoreRepository;
this._siteDefinitionResolver = siteDefinitionResolver;
}
/// <summary>
/// Determines whether the specified principal is a member of this role.
/// </summary>
/// <param name="principal">The principal.</param>
/// <param name="context">The context.</param>
/// <returns>
/// <c>true</c> if the specified principal is in the virtual role; otherwise, <c>false</c>.
/// </returns>
/// <note>
/// Any implementation of IsInVirtualRole must be fully thread-safe since only one instance of the class
/// is created and any role checks are made against the same instance.
/// </note>
public override bool IsInVirtualRole(IPrincipal principal, object context)
{
if (!principal.Identity.IsAuthenticated)
return false;
if (context is ContentAccessControlList cacl)
{
if (cacl.ContentLink == ContentReference.WasteBasket)
return true;
var originalParentLink = _parentRestoreRepository.GetParentLink(cacl.ContentLink);
/* Check parent access */
var parentContent = originalParentLink?.Get<IContent>();
if (parentContent == null)
return false;
if (parentContent.QueryDistinctAccess(AccessLevel.Read))
return true;
}
return false;
}
}
}
</code>
</pre>
<br />
<br />
<b><span style="color: red;">The Recylce bin role setup looked like</span></b><br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
<virtualRoles addClaims="true">
<providers>
<add name="SiteTrashRole" type="NRL.Web.Business.VirtualRoles.SiteTrashRole, NRL.Web.Business" />
</providers>
</virtualRoles>
</code>
</pre>
<br />
<br />
Now, when user opens up the recycle bin then it will see content of site it belonged to.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhl0bawwIIf-TsZQ1jgzgostS8BsHgSvDzFkhgLaP4JDWYJDNB3gfJ7jC8wFph7kKM3zVjptKL061IMQdcS4BegcZdBQTUhBT-0peICcsfaWB8621c_3jkJSOWXlB813NM_V4SV9eQPhUI/s1600/LimitedContent.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="484" data-original-width="897" height="172" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhl0bawwIIf-TsZQ1jgzgostS8BsHgSvDzFkhgLaP4JDWYJDNB3gfJ7jC8wFph7kKM3zVjptKL061IMQdcS4BegcZdBQTUhBT-0peICcsfaWB8621c_3jkJSOWXlB813NM_V4SV9eQPhUI/s320/LimitedContent.JPG" width="320" /></a></div>
<br />
<br />
<br />
@Note: We have give only read/change access to editors so they can see the lists. The restore and delete would not work as it requires delete access rights. The problem with given delete access rights is that delete button becomes enabled and then editor would be able to delete content of other sites.<br />
If you try to restore you will get below message<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiri__o96Dsk0bXrzchuPsDQrYaSofW6TMlzBUdRsSCmkuMkT1kCR_y2gb5qjqi2ypZddQZsFNaFoD4CcOeQDKSsxRLBwVj2I5K7wAbxAXMr4wMZKDkYQnqqhi1fgxkrE6lOEZPgCd__Ws/s1600/DeleteError.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="605" data-original-width="874" height="221" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiri__o96Dsk0bXrzchuPsDQrYaSofW6TMlzBUdRsSCmkuMkT1kCR_y2gb5qjqi2ypZddQZsFNaFoD4CcOeQDKSsxRLBwVj2I5K7wAbxAXMr4wMZKDkYQnqqhi1fgxkrE6lOEZPgCd__Ws/s320/DeleteError.JPG" width="320" /></a></div>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-4352545758060805082017-06-11T18:34:00.000+10:002017-06-11T18:40:08.152+10:00Drilldown Autosuggest Drop down Properties in EPiServer<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our projects we had to create drill down auto suggest drop down for EPiServer properties.<br />
We had three level of drill down based on parent and child relation. We wanted to use the existing auto suggest functionality available and tweak it little bit to fit our requirement.<br />
We decided to write dojo JS to change the functionality for suggestion drop down.<br />
<br />
How the Drop down looks like<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZqIokxDF35Y36iwOxrimISp93wE_u30VCh1U1WB9Pp4oIdFUTDg3Wm_dAVHGpLEBHUa4wDvJp-oc4PaPUxL_AIrFIA-Ic7G3xMPV3JksfD3qldnlKWom1LH1JI4EKD2MV6BDG9yCD-mQ/s1600/How+It+Looks+Like.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="243" data-original-width="673" height="143" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZqIokxDF35Y36iwOxrimISp93wE_u30VCh1U1WB9Pp4oIdFUTDg3Wm_dAVHGpLEBHUa4wDvJp-oc4PaPUxL_AIrFIA-Ic7G3xMPV3JksfD3qldnlKWom1LH1JI4EKD2MV6BDG9yCD-mQ/s400/How+It+Looks+Like.JPG" width="400" /></a></div>
<br />
<br />
<br />
Selection Model<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class SelectionModel : SelectItem
{
public string PrentId { get; set; }
}
</code>
</pre>
<br />
Selection Query Classes<br />
<br />
Competition Selection Query<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(ISelectionQuery))]
public class CompetitionSelectionQuery : ISelectionQuery
{
IEnumerable<iselectitem> _competitions;
public CompetitionSelectionQuery()
{
_competitions = new List<selectitem>
{
new SelectItem
{
Text = "Competition 1",
Value = "1"
},
new SelectItem
{
Text = "Competition 2",
Value = "2"
}
};
}
public ISelectItem GetItemByValue(string value)
{
return _competitions.FirstOrDefault(s => s.Value.ToString() == value);
}
public IEnumerable<iselectitem> GetItems(string query)
{
return _competitions.Where(s => s.Text.Contains(query));
}
}
</iselectitem></selectitem></iselectitem></code>
</pre>
<br />
Season Selection Query<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(ISelectionQuery))]
public class SeasonSelectionQuery : ISelectionQuery
{
IEnumerable<selectionmodel> _seasons;
public SeasonSelectionQuery()
{
_seasons = new List<selectionmodel>
{
new SelectionModel
{
Text = "Season 1",
Value = "1",
PrentId = "1"
},
new SelectionModel
{
Text = "Season 2",
Value = "2",
PrentId = "1"
},
new SelectionModel
{
Text = "Season 1",
Value = "3",
PrentId = "2"
},
new SelectionModel
{
Text = "Season 2",
Value = "4",
PrentId = "2"
},
};
}
public ISelectItem GetItemByValue(string value)
{
return _seasons.FirstOrDefault(s => s.Value.ToString() == value);
}
public IEnumerable<iselectitem> GetItems(string query)
{
var parent = query.Split(new[] { '-' })[0];
return _seasons.Where(s => s.Text.Contains(query));
}
}
</iselectitem></selectionmodel></selectionmodel></code>
</pre>
<br />
Round Selection Query<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(ISelectionQuery))]
public class RoundSelectionQuery : ISelectionQuery
{
IEnumerable<selectionmodel> _rounds;
public RoundSelectionQuery()
{
_rounds = new List<selectionmodel>
{
new SelectionModel
{
Text = "Round 1",
Value = "1",
PrentId = "1"
},
new SelectionModel
{
Text = "Round 2",
Value = "2",
PrentId = "1"
},
new SelectionModel
{
Text = "Round 1",
Value = "3",
PrentId = "2"
},
new SelectionModel
{
Text = "Round 2",
Value = "4",
PrentId = "2"
},
};
}
public ISelectItem GetItemByValue(string value)
{
return _rounds.FirstOrDefault(s => s.Value.ToString() == value);
}
public IEnumerable<iselectitem> GetItems(string query)
{
var parent = query.Split(new[] { '-' })[0];
return _rounds.Where(s => s.Text.Contains(query));
}
}
</iselectitem></selectionmodel></selectionmodel></code>
</pre>
<br />
How to define properties in Content<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[Display(Order = 110)]
[LinkedAutoSelection(typeof(CompetitionTagSelectionQuery), null, null, new[] { "/season/topic", "/round/topic"})]
public virtual string Competition { get; set; }
[LinkedAutoSelection(typeof(SeasonTagSelectionQuery), new[] { "competition" }, "/season/topic", new[] { "/round/topic" })]
[Display(Order = 120)]
public virtual string Season { get; set; }
[LinkedAutoSelection(typeof(RoundTagSelectionQuery), new[] { "competition", "season" }, "/round/topic", null)]
[Display(Order = 130)]
public virtual string Round { get; set; }
</code>
</pre>
<br />
<br />
Understanding LinkedAutoSelection Attribute<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class LinkedAutoSelectionAttribute : Attribute, IMetadataAware
{
internal Injected<episerver .shell.modules.moduletable=""> ModuleTable { get; set; }
public Type SelectionFactoryType { get; set; }
/* Dependent Parents: Child needs to pull data from parents to pass to the auto suggest query
* We will fetch the parents values based on input name */
public string[] Parents { get; set; }
/* The change in value will be published to all the publishers */
public string[] Publishers { get; set; }
/* The subscriber will reset the value based on parent selection */
public string Subscriber { get; set; }
public LinkedAutoSelectionAttribute(Type selectionFactoryType, string[] parents, string subscriber, string[] publishers)
{
SelectionFactoryType = selectionFactoryType;
this.Publishers = publishers;
this.Subscriber = subscriber;
this.Parents = parents;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
var extendedMetadata = metadata as ExtendedMetadata;
if (extendedMetadata == null)
return;
extendedMetadata.ClientEditingClass = "nrl/Editors/LinkedSelectionEditor/Editor";
extendedMetadata.CustomEditorSettings["uiType"] = (object)extendedMetadata.ClientEditingClass;
extendedMetadata.CustomEditorSettings["uiWrapperType"] = (object)"flyout";
string format = this.ModuleTable.Service.ResolvePath("Shell", "stores/selectionquery/{0}/");
extendedMetadata.EditorConfiguration["storeurl"] = (object)string.Format((IFormatProvider)CultureInfo.InvariantCulture, format, new object[1]
{
(object) this.SelectionFactoryType.FullName
});
extendedMetadata.EditorConfiguration["parents"] = Parents;
extendedMetadata.EditorConfiguration["publishers"] = Publishers;
extendedMetadata.EditorConfiguration["subscriber"] = Subscriber;
}
}
</episerver></code>
</pre>
<br />
<br />
Adding LinkedSelectionEditor Editor.Js File<br />
<br />
Setting up module.config<br />
<br />
In module.config the path to Resources/Scripts is defined as Murtaza and<br />
inside Resources/Scripts. I have Editors -> LinkedSelectionEditor -> Editor.js<br />
<div style="text-align: left;">
therefore, the<span style="background-color: yellow;"> <span style="font-family: "arial"; font-size: 12px;">ClientEditingClass is Murtaza/Editors/ImageContentSelector/Editor</span></span></div>
<div style="text-align: left;">
<span style="background-color: yellow;"><span style="font-family: "arial"; font-size: 12px;"><br /></span></span></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiL6vFKxzT9iy-BaF57VnS5p07FUX7_kU3Xa0WTkWp_GQPjjJjgUfND2oqmEP7sG5hGtyUa-6kNCumLpwN2emcb-3wZ04qY991y74REF7qS5wUqMFslLJPHpchyKTrEg7kF6S_TkF1WvjA/s1600/Resource.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="209" data-original-width="397" height="210" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiL6vFKxzT9iy-BaF57VnS5p07FUX7_kU3Xa0WTkWp_GQPjjJjgUfND2oqmEP7sG5hGtyUa-6kNCumLpwN2emcb-3wZ04qY991y74REF7qS5wUqMFslLJPHpchyKTrEg7kF6S_TkF1WvjA/s400/Resource.JPG" width="400" /></a></div>
<div style="text-align: left;">
<span style="background-color: yellow;"><span style="font-family: "arial"; font-size: 12px;"><br /></span></span></div>
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
<module clientresourcerelativepath="" tags="EPiServerPublicModulePackage">
<assemblies>
<add assembly="XXX">
<add assembly="XXX">
</add></add></assemblies>
<dojo>
<paths>
<add name="Murtaza" path="Resources/Scripts">
</add></paths>
</dojo>
<clientmodule>
<moduledependencies>
<add dependency="CMS" type="RunAfter">
</add></moduledependencies>
</clientmodule>
</module>
</code>
</pre>
<br />
Editor.Js<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
define("nrl/Editors/LinkedSelectionEditor/Editor",[
"dojo/_base/declare",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"epi-cms/_ContentContextMixin",
"dojo/topic",
"dojo/_base/array",
"epi/shell/form/AutoCompleteSelectionEditor"
],
function (
declare,
_Widget,
_TemplatedMixin,
_ContentContextMixin,
topic,
array,
AutoCompleteSelectionEditor
) {
return declare([
_Widget,
_TemplatedMixin,
_ContentContextMixin,
AutoCompleteSelectionEditor,
], {
/* delimeter : String
* Delimeter for value and page name
* Competition111 - NRL(NRL is the name of page)*/
delimeter: "-",
oldValue: "",
postMixInProperties: function () {
this.inherited(arguments);
var that = this;
if (this.subscriber) {
topic.subscribe(this.subscriber, function () {
var found = false;
var argument = arguments[0];
/* We are in context of subscriber and as we have recieved notification for changed in parent value therefore,
* we are going to reset current dropdown
* clearing the value */
/* Clearing value */
that._setValueAttr('', true, '');
/* Clearing display value */
that._setDisplayedValueAttr('');
});
}
},
_setDisplayedValueAttr: function (/*String*/ label, /*Boolean?*/ priorityChange) {
/* If label is setting as '' then we need to verify if textbox exits.
* in some of the other functions during life cycle textbox does not exists
* After clearing up display value we are not going to call base function as it will start fetching
* list from server unnecessary and throws exceptions*/
if (label === '') {
if (this.textbox)
this.textbox.value = label;
return;
}
this.inherited(arguments);
},
_selectOption: function (/*DomNode*/ target) {
this.inherited(arguments);
var that = this;
if (this.getValue() != this.oldValue)
{
/* Publish the change to all pubilshers */
array.forEach(this.publishers, function (publisher) {
/* Publishing Change
* Create key value pair */
var keyValue = new Object();
keyValue.key = that.subscriber;
keyValue.value = that.value;
topic.publish(publisher, keyValue);
});
}
},
_setValueAttr: function (/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue, /*item?*/ item) {
/* Saving the old value */
this.oldValue = this.getValue();
this.inherited(arguments);
},
_startSearch: function (/*String*/ text) {
/* Competition111-Season2017-Round06-{text} */
/* Prefixing all the pre-requisite parent values */
var prefixQuery = '';
/* Fetch values from parents */
for (var i = 0; this.parents && this.parents.length > i; i++) {
var dependantValues = document.getElementsByName(this.parents[i]);
if (dependantValues.length > 0) {
prefixQuery += this.removePageName(dependantValues[0].value) + this.delimeter;
}
}
/* Removing the first & last {this.delimeter} character (^\-|\-+$)/g */
var regex = new RegExp('(^\\' + this.delimeter + '|\\' + this.delimeter + '+$)', 'g');
prefixQuery = prefixQuery.replace(regex, '');
prefixQuery = (prefixQuery + this.delimeter + text).replace(regex, '');
/* Reassigning search text with all pre-requisite values
* Keeping old value to reassign later */
var oldText = text;
text = prefixQuery;
this.inherited(arguments);
/* Reassigning old value this property will be used in base class */
this._lastInput = oldText;
},
/* Remove the page name suffixed after {this.delimeter} character */
removePageName: function (val)
{
var trimmedValue = val;
var typeAndId = trimmedValue.split(this.delimeter);
if (typeAndId.length > 0) {
trimmedValue = typeAndId[0];
}
return trimmedValue;
}
});
});
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-69970530209697415432017-05-11T23:08:00.000+10:002017-05-31T13:59:00.892+10:00Enabling Opening Of Image Editor With Single/Double Click For Image Property in EPiServer<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our project we had to extend functionality to extend the content selector feature to enable to open up the image editor with single/double. The reason for extending feature is to enable quick access to image editor instead of browsing through right hand panel.<br />
<h2 style="text-align: left;">
<span style="color: red;"><br />How it is look like</span></h2>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjurZcE5WHTVAb_APKdiFfFbagqIMsDjjkqOQfJjUWme6wNro3AwCHjMd1-NKFnQF2nh81RgvR3NXFXEJRZF9hxKakdIoaOfid0JnefmcOdEX3-X65ye6T5nlN7v-mFafunUGP-M8h5ogU/s1600/ImageSelector.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="60" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjurZcE5WHTVAb_APKdiFfFbagqIMsDjjkqOQfJjUWme6wNro3AwCHjMd1-NKFnQF2nh81RgvR3NXFXEJRZF9hxKakdIoaOfid0JnefmcOdEX3-X65ye6T5nlN7v-mFafunUGP-M8h5ogU/s400/ImageSelector.JPG" width="400" /></a></div>
<h2 style="text-align: left;">
<span style="color: red;"><br />Defining Image Porpety as ContentReference</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[UIHint(UIHint.Image)]
[Display(Order = 40)]
public virtual ContentReference Image { get; set; }
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;"><br />Extending Image Property Descriptor</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <summary>
/// Extends functionality of image content selector to open up image editor on single/double click
/// </summary>
[EditorDescriptorRegistration(TargetType = typeof(ContentReference), UIHint = UIHint.Image)]
public class ImageEditorDescriptor : EditorDescriptor
{
public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<attribute> attributes)
{
metadata.ClientEditingClass = "Murtaza/Editors/ImageContentSelector/Editor";
base.ModifyMetadata(metadata, attributes);
}
}
</attribute></code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;"><br />Defining Javascript class</span></h2>
In module.config the path to Resources/Scripts is defined as Murtaza and<br />
inside Resources/Scripts. I have Editors -> ImageContentSelector -> Editor.js<br />
therefore, the <span style="background-color: #f0f0f0; font-family: "arial"; font-size: 12px;">ClientEditingClass is Murtaza/Editors/ImageContentSelector/Editor</span><br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
<dojo>
<paths>
<add name="Murtaza" path="Resources/Scripts">
</add></paths>
</dojo>
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;"><br />Extending Dojo Functionality</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
define([
"dojo/_base/declare",
"dojo/dom-style",
"dojo/aspect",
"epi-cms/widget/ContentSelector"
],
/* Extends functionality of image content selector to open up image editor on single/double click */
function (
declare,
domStyle,
aspect,
ContentSelector
) {
return declare([ContentSelector], {
postCreate: function () {
this.inherited(arguments);
/* Display node is the attach point of outer div */
this.displayNode.addEventListener("click", this.openImageEditor.bind(this))
this.displayNode.addEventListener("dbclick", this.openImageEditor.bind(this))
},
_updateDisplayNode: function (content) {
/* Inherited from _SelectorBase through ContentSelector */
this.inherited(arguments);
/* Saving it for later to extract content id to form uri */
this.content = content;
},
openImageEditor: function (e)
{
if (this.content && this.content.uri) {
var contextParameters = { uri: this.content.uri };
dojo.publish("/epi/shell/context/request", [contextParameters, { sender: this }]);
}
},
});
});
</code>
</pre>
<br />
<br />
In order to load another content in edit mode we need to send request using dojo.publish function.<br />
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-8772509598140679762017-04-26T23:13:00.000+10:002017-05-11T22:15:24.046+10:00EPiServer Custom XForm Column Download Control - Dojo Control<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
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.<br />
Client wanted to download only selected columns even though we were recording lots of information in XForm.<br />
<br />
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.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">How it is look likes</span></h2>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjQuYmZmevQt6cQ9lfT5ObTYHya5dAuUthRGttS0cEDtaejDvu_32VReIrFycb7SEbOWDzlbdd9zAX5QiDWnPz6M6lsiloYmbvhBtVYa80tOjBehHqjwJlpKcaGFIlVQ9YgsWW68LlQdM/s1600/CustomColumnXFormControl.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="102" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjQuYmZmevQt6cQ9lfT5ObTYHya5dAuUthRGttS0cEDtaejDvu_32VReIrFycb7SEbOWDzlbdd9zAX5QiDWnPz6M6lsiloYmbvhBtVYa80tOjBehHqjwJlpKcaGFIlVQ9YgsWW68LlQdM/s400/CustomColumnXFormControl.PNG" width="400" /></a></div>
<br />
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">How to define property</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[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; }
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">PropertyXFormCustomColumnDownloadButton</span></h2>
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.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[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;
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">XFormCustomColumnDownloadButtonEditorDescriptor</span></h2>
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.<br />
<div>
<br /></div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <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);
}
}
</code><span style="color: red;">
</span></pre>
<h2 style="text-align: left;">
<span style="color: red;"><br />XFormReferencedPropertyAttribute</span></h2>
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.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[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;
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">XFormCustomColumnExport</span></h2>
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.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <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
{
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Adding Js File and Updating module.config</span></h2>
We need to created Js File inside ClientResources -> Scripts -> Editors<br />
and need to update module.config to let EPiServer know form where to fetch the file.<br />
JS File for Custom XForm Download With Date Controls<br />
<br />
<b>template.editors.XFormCustomColumnDownload</b><br />
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnJgpHDhc2gUa479dEPbxx9DDPwIflewJ-vXjroPj-144-eBEVyIwsi76i14q4DjUb9ZFDfcX3bLMksr5Tn4nYmH-fX4_kuBaWxQhbjpunSEmE4UbMSkCMllONz1VHQRCGd6qjZefYfQ4/s1600/Dojo+Name.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="152" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnJgpHDhc2gUa479dEPbxx9DDPwIflewJ-vXjroPj-144-eBEVyIwsi76i14q4DjUb9ZFDfcX3bLMksr5Tn4nYmH-fX4_kuBaWxQhbjpunSEmE4UbMSkCMllONz1VHQRCGd6qjZefYfQ4/s320/Dojo+Name.JPG" width="320" /></a></div>
<b><br /></b><b>The structure of Dojo Control name is as follows </b><b><span style="color: red;">template.editors.XFormCustomColumnDownload</span></b><br />
<b>The template in name is from module.config, the editors is the folder inside the ClientResources->Scripts folder and </b><b>XFormCustomColumnDownload is the name of js file.</b><br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/*
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);
}
});
});
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">module.config</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"><?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>
</code></pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">ExportDataController</span></h2>
I have Got ExportDataController under XFormCustomColumnDataExport Area. I am passing relative url to Js File through XFormCustomColumnDownloadButtonEditorDescriptor.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <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");
}
</code><span style="color: red;">
</span></pre>
<h2 style="text-align: left;">
<span style="color: red;"><br />Helper function
</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/// <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", " ");
}
</code> </pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-78118756663883899682017-04-10T22:53:00.000+10:002017-04-10T22:54:02.463+10:00Adding Tax and Jurisdiction in EPiServer Commerce through Code<div dir="ltr" style="text-align: left;" trbidi="on">
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.<br />
<br />
Therefore, i wrote little Migration Step to add Australian Tax Jurisdiction Group, Jurisdiction and Value.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[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;
}
</code>
</pre>
<br />
In AddJurisdictionAndTax we will first delete any existing australian Tax and Jurisdiction and then will re-add it again.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
</code>
</pre>
<br />
<br />
Removing the Existing Australian Jurisdiction and Tax<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
</code>
</pre>
<br />
Adding Australian Jurisdiction and Tax<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
</code>
</pre>
<br />
<br />
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.</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-64998066849496792532017-03-30T00:33:00.000+11:002017-03-30T00:34:01.963+11:00.Net Core Entity Framework Seperate Entity and Mapping Class<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
Follow the below steps to have separate entity and mapping definition for entity framework.<br />
<br />
This blog assumes you have prior knowledge of the .Net Entity Framework and you have ApplicationDBContext created already.<br />
<br />
First we will have BaseEntity Class<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
namespace TogetherWeCan.Data.IdentityManagement.Model
{
public class EntityBase
{
}
}
</code>
</pre>
<br />
Then, we will have EntityMappingBase class with virtual function accpeting ModelBuilder as a parameter.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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>();
}
}
}
</code></pre>
<br />
We are going to define two Entity Class.<br />
<br />
<ol style="text-align: left;">
<li>Group</li>
<li>GroupTwo</li>
</ol>
<div>
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
namespace TogetherWeCan.Data.IdentityManagement.Model
{
public class Group : EntityBase
{
public Guid GroupId { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
}
}</code></pre>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">namespace TogetherWeCan.Data.IdentityManagement.Model
{
public class GroupTwo : EntityBase
{
public Guid GroupTwoId { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
}
}
</code>
</pre>
<br />
Next we will define two mapping for Group and GroupTwo entity class.</div>
<div>
<br /></div>
<div>
<ol style="text-align: left;">
<li>GroupMapping</li>
<li>GroupTwoMapping</li>
</ol>
<div>
<br /></div>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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();
}
}
}</code></pre>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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();
}
}
} </code>
</pre>
</div>
<br />
<br />
Now we will have ApplicationDBContext class to initialize above mapping<br />
In order to get the runtime assembly and class i have used below packages<br />
<br />
For <span style="color: red;">RuntimeEnvironment.GetRuntimeIdentifier()</span> I have used Microsoft.DotNet.InternalAbstractions;<br />
<br />
For <span style="color: red;">DependencyContext.Default.GetRuntimeAssemblyNames(runtimeId).Where(w => w.FullName.Contains("TogetherWeCan.Data"));</span> I have used Microsoft.Extensions.DependencyModel;<br />
<div>
<br /></div>
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span><span style="font-size: 12px;">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);
}
}
}
</span><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span></code><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span></pre>
</div>
<br />
<br />
Testing<br />
<br />
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.<br />
<br />
<br /></div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span><span style="font-size: 12px;">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?}");
});
}
}
}
</span><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span></code><span style="font-family: "arial";"><span style="font-size: 12px;">
</span></span></pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com2tag:blogger.com,1999:blog-8525160902548525190.post-41211159624152584692017-03-19T00:06:00.000+11:002017-03-29T17:22:19.050+11:00Entity Framework Code First Unit/Integration Testing Using Effort<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
We can unit test Entity Framework Code First using Effort.<br />
<div>
Effort can be installed in test project using nuget package <a href="https://www.nuget.org/packages/Effort.EF6/">https://www.nuget.org/packages/Effort.EF6/ </a></div>
<div>
<br /></div>
<div>
Following below steps we can have basic EF code first unit test.</div>
<div>
<br /></div>
<div>
<h2 style="text-align: left;">
<span style="color: red;">Create Student Domain Class</span></h2>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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; }
}
</code>
</pre>
<br />
<div>
<h2 style="text-align: left;">
<span style="color: red;">Create Mapping Class</span></h2>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class StudentMap : EntityTypeConfiguration<student>
{
public StudentMap()
{
this.ToTable("Student");
this.HasKey<int>(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();
}
}
</int></student></code>
</pre>
</div>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Create School Context Class</span></h2>
<div style="text-align: left;">
Note: There is DatabaseInitializer which is drop and create always required for unit testing.</div>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class SchoolContext : DbContext
{
public SchoolContext() : base()
{
}
/// <summary> /// We need this constructor to pass the Effort dbConnection to process data in memory
/// </summary> public SchoolContext(DbConnection dbConnection) : base(dbConnection, true)
{
Database.SetInitializer<schoolcontext>(
new DropCreateDatabaseAlways<schoolcontext>());
}
public DbSet<student> Student { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new StudentMap());
base.OnModelCreating(modelBuilder);
}
}
</student></schoolcontext></schoolcontext></code>
</pre>
</div>
<br />
Create StudentRepository and Student Service class to perform Insert/Update Operations.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">IStudentRepository and StudentRepository</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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;
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">IStudentService and StudentService</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">Unit Test Class</span></h2>
<div style="text-align: left;">
Add Effort.EF6 Nuget package to unit test library</div>
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[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);
}
}
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com26tag:blogger.com,1999:blog-8525160902548525190.post-90037962132691151972017-02-07T01:20:00.003+11:002017-02-07T01:21:11.070+11:00Add Custom Element, Extend Existing Element With Custom Attributes in EPiServer TinyMCE <div dir="ltr" style="text-align: left;" trbidi="on">
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<br />
<br />
Fortunately, EPiServer provides easy way to Add and Extend TinyMCE element and attributes.<br />
<br />
First of all we need to define below class<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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
{
}
}
</code>
</pre>
<br />
<br />
Then, we need to define this EmptyHandler other wise js will throw error<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
using System.Web;
namespace EPiServer7App.EPiServerCore.Core.TinyMce
{
public class EmptyFileHandler : IHttpHandler
{
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
}
}
}
</code>
</pre>
<br />
Then, we need to update the web.config location settings<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> <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>
</code></pre>
<br />
Then, we will update the episerver.config file to allow TinyMCE merge of custom element.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"><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>
</code>
</pre>
<br />
After all of the above steps TinyMCE will not strip off the custom elements and attibutes.</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-288617775630035952017-02-03T00:10:00.000+11:002017-02-03T00:10:33.449+11:00Creating Split Shipment, Address For Line Item in EPiServer<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our project I had to create multiple shipment and addresses depending on the line item type.<br />
<br />
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.<br />
<br />
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.<br />
<br />
<h2 style="text-align: left;">
<b><span style="color: red;">Adding Order Address For Each Line Item</span></b></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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 */
}
</code></pre>
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;"><b>Adding Shipment and Assigning Shipment to Line Item</b></span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
/* 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();
</code>
</pre>
<br />
<h2 style="text-align: left;">
<b><span style="color: red;">Adding Shipping Method, Shipping Rate and Assigning it to Line Item</span></b></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
</code>
</pre>
<br />
<br />
<h2 style="text-align: left;">
<b><span style="color: red;">GetShippingMethod </span></b></h2>
The above method gets the rate for shipping method. This is the function available in QuickSilver template. I have included here as well<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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);
}
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-35056158031488710612017-01-30T01:47:00.000+11:002017-01-30T01:47:00.384+11:00Extending EPiServer Commerce Classes<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
In EPiServer we can extend the existing Commerce Classes. Such as <span style="color: red;">PurchaseOrder</span>, <span style="color: red;">OrderForm </span>and <span style="color: red;">LineItem</span>.<br />
<br />
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.<br />
for e.g<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
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;
</code></pre>
</div>
</div>
<br />
First all of we need to define create the custom meta class and custom meta property in Commerce Section.<br />
<br />
We will assign the associated meta property to meta classes.<br />
<br />
We will have following custom classes<br />
<br />
<ul style="text-align: left;">
<li>TcmsOrderForm</li>
<li>TcmsLineItem</li>
</ul>
<br />
We will also have following meta properties(Not all are included)<br />
<br />
<ul style="text-align: left;">
<li>StockRoomId</li>
<li>PeriodId</li>
<li>UnitOfMeasure</li>
<li>IsPrescribed</li>
</ul>
<br />
Meta properties will need to be assigned to associated Meta classes in Commerce section.<br />
Once we have meta property and meta classes relation defined we are ready to create extended commerce classes.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">Extended Purchase Order (TcmsPurchaseOrder)</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">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; }
}
}
}
</code></pre>
<br />
<h2>
<span style="color: red;">Extended Order Form (TcmsOrderForm)</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">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);
}
}
}
</code></pre>
<br />
<h2>
<span style="color: red;">Extended Line Item (TcmsLineItem)</span></h2>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">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; }
}
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">ecf.order.config</span></h2>
We also need to update the ecf.order.config section to include the custom class definition<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"><?xml version="1.0"?>
<Orders newOrderStatus="Draft" autoConfigure="true" shipmentAutoReleaseTimeout="1:0:0">
<MappedTypes>
<ShoppingCartType name="Mediachase.Commerce.Orders.Cart,Mediachase.Commerce" />
<!-- Custom Lines -->
<span style="background-color: yellow;"><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" /> </span>
<!-- 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>
</code></pre>
</div>
<br />
After defining custom classes and configuration we are now ready to use the classes<br />
Below are the utility functions and example to use the above classes.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="word-wrap: normal;">
<code style="color: black; word-wrap: normal;">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;
</code></code></pre>
<br />
Extension method to convert normal classes to extended classes<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">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;
}
}
}
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-5229787349567599242017-01-24T01:38:00.002+11:002017-01-24T01:38:49.572+11:00Adding EPiServer Meta Property through Code And Assigning it to MetaClass<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
In this post, we will go through the feature to add meta property to OrderForm, LineItem and CredtiCardPayment class.<br />
<br />
We are going to use <b>MetaClass </b>and <b>MetaField </b>class of <b>Mediachase.MetaDataPlus</b> to add/update property to class<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
var metaLineItemClass = MetaClass.Load(MetaDataContext.Instance, "LineItemEx")
</code>
</pre>
<br />
The above function will return the instance of LineItemEx class. Using this instance you can add meta field to give meta class.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
var metaProp = MetaField.Load(MetaDataContext.Instance, "GiftVoucherCode")
</code>
</pre>
<br />
<br />
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.<br />
<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
<div style="font-family: "Times New Roman"; font-size: medium; white-space: normal;">
if (metaProp == null)</div>
<div style="font-family: "Times New Roman"; font-size: medium; white-space: normal;">
metaProp = MetaField.Create(MetaDataContext.Instance, "Mediachase.Commerce.Orders", "Gift Voucher Code", "Gift Voucher Code", "Gift Voucher Code", MetaDataType.ShortString, 100, true, false, false, false);</div>
<div style="font-family: "Times New Roman"; font-size: medium; white-space: normal;">
</div>
<div style="font-family: "Times New Roman"; font-size: medium; white-space: normal;">
if (!metaLineItemClass.MetaFields.Contains(metaProp))</div>
<div style="font-family: "Times New Roman"; font-size: medium; white-space: normal;">
metaLineItemClass.AddField(metaProp);</div>
</code>
</pre>
<br />
<br />
The above piece of code first checks if the meta field is already created. If not then it creates one.<br />
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.<br />
<br />
Below is the complete code that I have used in my project.<br />
<br />
We will have class <span style="color: red;"><b>MetaClassAndFieldsSyncInitializer </b></span>which will add property to meta class.<br />
<br />
<br /></div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> 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;
}
}
}
</code></pre>
</div>
<br />
We will also this <span style="color: red; font-weight: bold;">SharedDependencyInitialization </span>which will be call once before initialization of site to add/update any meta class or meta field.<br />
<b><span style="color: red;"><br /></span></b>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="word-wrap: normal;">
<code style="color: black; word-wrap: normal;">
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();
}
}
}
</code></pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com2tag:blogger.com,1999:blog-8525160902548525190.post-5246398462076783552016-12-07T01:15:00.001+11:002016-12-07T01:16:33.390+11:00Using ExpandoObject with Dictionary to get rid of magic string<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
In most of the project we used the dictionary and along with dictionary there comes magic string.<br />
<br />
In order to get rid of the magic string from code I utilize the ExpandoObject to add/update dictionary item by converting ExpandoObject into dynamic instance.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">Usage</span></h2>
<div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
Dictionary<string, string> dictoinary = new Dictionary<string, string>();
dictoinary["FName"] = "Murtaza";
dictoinary["LName"] = "Ali";
dictoinary["DOB"] = "1/1/1001";
var dynamicDictioinary = new DictionaryExpando<string>(dictoinary) as dynamic ;
dynamicDictioinary.Gender = "Definitely Not Female";
Console.WriteLine(dynamicDictioinary.FName);
dynamicDictioinary.FName = "Murtaza Override";
Console.WriteLine(dynamicDictioinary.FName);
Console.WriteLine(dynamicDictioinary.Gender);
</code></pre>
<h2>
<span style="color: red;">DictionaryExpando Class</span></h2>
</div>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[Serializable]
class DictionaryExpando<TValue> : DynamicObject
{
/// <summary>
/// Instance of object passed in
/// </summary>
public Dictionary<string, TValue> Instance;
/// <summary>
/// Cached type of the instance
/// </summary>
private Type InstanceType;
private PropertyInfo[] InstancePropertyInfo
{
get
{
if (_InstancePropertyInfo == null && Instance != null)
_InstancePropertyInfo =
Instance.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
return _InstancePropertyInfo;
}
}
private PropertyInfo[] _InstancePropertyInfo;
public Dictionary<string,TValue> Properties = new Dictionary<string, TValue>();
/// <summary>
/// Allows passing in an existing instance variable to 'extend'.
/// </summary>
/// <remarks>
/// You can pass in null here if you don't want to
/// check native properties and only check the Dictionary!
/// </remarks>
/// <param name="instance"></param>
public DictionaryExpando(Dictionary<string, TValue> instance)
{
Initialize(instance);
}
protected virtual void Initialize(Dictionary<string, TValue> instance)
{
Instance = instance;
if (instance != null)
InstanceType = instance.GetType();
}
/// <summary>
/// Try to retrieve a member by name first from instance properties
/// followed by the collection entries.
/// </summary>
/// <param name="binder"></param>
/// <param name="result"></param>
/// <returns></returns>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = default(TValue);
// first check the Properties collection for member
if (Properties.Keys.Contains(binder.Name))
{
result = Properties[binder.Name];
return true;
}
// Next check for Public properties via Reflection
if (Instance != null)
{
try
{
return GetProperty(Instance, binder.Name, out result);
}
catch
{
}
}
// failed to retrieve a property
result = null;
return false;
}
/// <summary>
/// Property setter implementation tries to retrieve value from instance
/// first then into this object
/// </summary>
/// <param name="binder"></param>
/// <param name="value"></param>
/// <returns></returns>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
// first check to see if there's a native property to set
if (Instance != null)
{
try
{
bool result = SetProperty(Instance, binder.Name, value);
if (result)
return true;
}
catch
{
}
}
// no match - set or add to dictionary
Properties[binder.Name] = (TValue)value;
return true;
}
/// <summary>
/// Dynamic invocation method. Currently allows only for Reflection based
/// operation (no ability to add methods dynamically).
/// </summary>
/// <param name="binder"></param>
/// <param name="args"></param>
/// <param name="result"></param>
/// <returns></returns>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if (Instance != null)
{
try
{
// check instance passed in for methods to invoke
if (InvokeMethod(Instance, binder.Name, args, out result))
return true;
}
catch
{
}
}
result = null;
return false;
}
/// <summary>
/// Reflection Helper method to retrieve a property
/// </summary>
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="result"></param>
/// <returns></returns>
protected bool GetProperty(object instance, string name, out object result)
{
if (instance == null)
instance = this;
result = default(TValue);
try
{
var dictonary = instance as Dictionary<string, TValue>;
result = dictonary[name];
return true;
}
catch (Exception)
{
}
return false;
}
/// <summary>
/// Reflection helper method to set a property value
/// </summary>
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
protected bool SetProperty(object instance, string name, object value)
{
if (instance == null)
instance = this;
try
{
Dictionary<string, TValue> dictionary = instance as Dictionary<string, TValue>;
dictionary[name] = (TValue)value;
return true;
}
catch (Exception)
{
}
return false;
}
/// <summary>
/// Reflection helper method to invoke a method
/// </summary>
/// <param name="instance"></param>
/// <param name="name"></param>
/// <param name="args"></param>
/// <param name="result"></param>
/// <returns></returns>
protected bool InvokeMethod(object instance, string name, object[] args, out object result)
{
if (instance == null)
instance = this;
// Look at the instanceType
var miArray = InstanceType.GetMember(name,
BindingFlags.InvokeMethod |
BindingFlags.Public | BindingFlags.Instance);
if (miArray != null && miArray.Length > 0)
{
var mi = miArray[0] as MethodInfo;
result = mi.Invoke(Instance, args);
return true;
}
result = null;
return false;
}
/// <summary>
/// Convenience method that provides a string Indexer
/// to the Properties collection AND the strongly typed
/// properties of the object by name.
///
/// // dynamic
/// exp["Address"] = "112 nowhere lane";
/// // strong
/// var name = exp["StronglyTypedProperty"] as string;
/// </summary>
/// <remarks>
/// The getter checks the Properties dictionary first
/// then looks in PropertyInfo for properties.
/// The setter checks the instance properties before
/// checking the Properties dictionary.
/// </remarks>
/// <param name="key"></param>
///
/// <returns></returns>
public object this[string key]
{
get
{
try
{
// try to get from properties collection first
return Properties[key];
}
catch (KeyNotFoundException ex)
{
// try reflection on instanceType
object result = null;
if (GetProperty(Instance, key, out result))
return result;
// nope doesn't exist
throw;
}
}
set
{
if (Properties.ContainsKey(key))
{
Properties[key] = (TValue)value;
return;
}
// check instance for existance of type first
var miArray = InstanceType.GetMember(key, BindingFlags.Public | BindingFlags.GetProperty);
if (miArray != null && miArray.Length > 0)
SetProperty(Instance, key, value);
else
Properties[key] = (TValue)value;
}
}
/// <summary>
/// Returns and the properties of
/// </summary>
/// <param name="includeProperties"></param>
/// <returns></returns>
public IEnumerable<KeyValuePair<string, object>> GetProperties(bool includeInstanceProperties = false)
{
if (includeInstanceProperties && Instance != null)
{
foreach (var prop in this.InstancePropertyInfo)
yield return new KeyValuePair<string, object>(prop.Name, prop.GetValue(Instance, null));
}
foreach (var key in this.Properties.Keys)
yield return new KeyValuePair<string, object>(key, this.Properties[key]);
}
/// <summary>
/// Checks whether a property exists in the Property collection
/// or as a property on the instance
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public bool Contains(KeyValuePair<string, object> item, bool includeInstanceProperties = false)
{
bool res = Properties.ContainsKey(item.Key);
if (res)
return true;
if (includeInstanceProperties && Instance != null)
{
foreach (var prop in this.InstancePropertyInfo)
{
if (prop.Name == item.Key)
return true;
}
}
return false;
}
}
</code>
</pre>
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">Instance</span></h2>
The <b>Isntance </b>property will return the real dictionary object to perform additional operations if required. You may need convert dynamic object back into ExpandoObject<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; font-family: arial; font-size: 12px; word-wrap: normal;"> public Dictionary<string, TValue> Instance;
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">TryGetMember and GetProperty</span></h2>
<b>TryGetMember </b>is overridden method of ExpandoObject in order to fetch the value of provided propertyname. The dynamic property name can be retrieved from binder.Name.<br />
<br />
<b>GetProperty </b>fetches the value from dictionary for provided key.<br />
<br />
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = default(TValue);
// first check the Properties collection for member
if (Properties.Keys.Contains(binder.Name))
{
result = Properties[binder.Name];
return true;
}
// Next check for Public properties via Reflection
if (Instance != null)
{
try
{
return GetProperty(Instance, binder.Name, out result);
}
catch
{
}
}
// failed to retrieve a property
result = null;
return false;
}
protected bool GetProperty(object instance, string name, out object result)
{
if (instance == null)
instance = this;
result = default(TValue);
try
{
var dictonary = instance as Dictionary<string, TValue>;
result = dictonary[name];
return true;
}
catch (Exception)
{
}
return false;
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">TrySetMember and SetProperty</span></h2>
<b>TrySetMember </b>is overridden method of ExpandoObject in order to set the value of provided propertyname. The dynamic property name can be retrieved from binder.Name.<br />
<br />
<b>SetProperty </b>sets the value to dictionary for provided key<br />
<br /></div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public override bool TrySetMember(SetMemberBinder binder, object value)
{
// first check to see if there's a native property to set
if (Instance != null)
{
try
{
bool result = SetProperty(Instance, binder.Name, value);
if (result)
return true;
}
catch
{
}
}
// no match - set or add to dictionary
Properties[binder.Name] = (TValue)value;
return true;
}
protected bool SetProperty(object instance, string name, object value)
{
if (instance == null)
instance = this;
try
{
Dictionary<string, TValue> dictionary = instance as Dictionary<string, TValue>;
dictionary[name] = (TValue)value;
return true;
}
catch (Exception)
{
}
return false;
}
</code>
</pre>
<br />
<b>Note</b>: If you wan to use the remove function of dictionary then you may still need to use the magic string to remove the key from dictionary.</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-89378076036564825282016-12-03T21:20:00.000+11:002016-12-03T21:20:12.751+11:00Make Every Page In EPiServer As Draggable As Single Tile<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our EPiServer Project we wanted to make every page as draggable and wanted to make them appear as single tile. Therefore, I came up with approach, In this approach we will make every base page available to implement <b>ISingleTileView </b>interface and we would register one template coordinator for given Interface.<br />
<br />
<h2>
<span style="color: red;">ISingleTileView</span></h2>
<br /></div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;"> public interface ISingleTileView
{
string Title { get; }
string TileDescription { get; }
string ImageUrl { get; }
string LinkToUrl { get; }
}
</code>
</pre>
<br />
We had 5 different base classes for CMS and ECommerce. All of them are listed below.<br />
<br />
<ol style="text-align: left;">
<li>SitePageData</li>
<li>SiteBlockData</li>
<li>BaseVariantContent</li>
<li>BaseProductContent</li>
<li>BaseCategory</li>
</ol>
<div>
The Class definitions of above all classes are defined as</div>
<div>
<br /></div>
<div>
<h2 style="text-align: left;">
<span style="color: red;">SitePageData</span></h2>
</div>
</div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public abstract class SitePageData : PageData, ISingleTileView
{
public string Title { get; }
public string TileDescription { get; }
public string ImageUrl { get; }
public string LinkToUrl { get; }
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">SiteBlockData</span></h2>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public abstract SiteBlockData : BlockData, ISingleTileView
{
public string Title { get; }
public string TileDescription { get; }
public string ImageUrl { get; }
public string LinkToUrl { get; }
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">BaseVariantContent</span></h2>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class BaseVariantcContent : VariationContent, ISingleTileView
{
public string Title { get; }
public string TileDescription { get; }
public string ImageUrl { get; }
public string LinkToUrl { get; }
}</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">BaseProductContent</span></h2>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class BaseProductContent : ProductContent, ISingleTileView
{
public string Title { get; }
public string TileDescription { get; }
public string ImageUrl { get; }
public string LinkToUrl { get; }
}
</code>
</pre>
<br />
<h2>
<span style="color: red;">BaseCategory</span></h2>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
public class BaseCategory : NodeContent, ISingleTileView
{
public string Title { get; }
public string TileDescription { get; }
public string ImageUrl { get; }
public string LinkToUrl { get; }
}
</code></pre>
</div>
<br />
You can populate above properties with what ever other properties.<br />
<br />
We have used below CMS properties to return value of <b>ISingleTileView </b>properties<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">Assigning Value To ISingleTileView Properties</span></h2>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
#region ISingle Tile View
[Ignore]
[ScaffoldColumn(false)]
public string Title
{
get { return Heading; }
}
[Ignore]
[ScaffoldColumn(false)]
public string TileDescription
{
get { return Abstract.ToHtmlString(); }
}
[Ignore]
[ScaffoldColumn(false)]
public string ImageUrl
{
get { return this.GetDefaultAsset<icontentmedia>(AssetMediaNames.THUMBNAIL).GetFriendlyUrl(); }
}
[Ignore]
[ScaffoldColumn(false)]
public string LinkToUrl
{
get { return ContentLink.GetFriendlyUrl(); }
}
#endregion
</icontentmedia></code><span style="color: red;">
</span></pre>
<h2 style="text-align: left;">
<span style="color: red;">TemplateCoordinator</span></h2>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ServiceConfiguration(typeof(IViewTemplateModelRegistrator))]
public class TemplateCoordinator : IViewTemplateModelRegistrator
{
public const string BlockFolder = "~/Views/Shared/Blocks/";
public const string PagePartialsFolder = "~/Views/Shared/PagePartials/";
public void Register(TemplateModelCollection viewTemplateModelRegistrator)
{
// All Pages will be Single Tile View will rendered below Partial view
viewTemplateModelRegistrator.Add(typeof(ISingleTileView), new TemplateModel()
{
Name = "Single Tile View For Any Page",
Inherit = true,
Tags = new[] { Constants.TemplateDescriptorTags.SingleTile },
Path = BlockPath("Single.Tile.cshtml"),
TemplateTypeCategory = TemplateTypeCategories.MvcPartialView,
AvailableWithoutTag = true,
Default = false
});
public static string BlockPath(string fileName)
{
return string.Format("{0}{1}", BlockFolder, fileName);
}
public static string PagePartialPath(string fileName)
{
return string.Format("{0}{1}", PagePartialsFolder, fileName);
}
}
</code>
</pre>
<br />
<h2 style="text-align: left;">
<span style="color: red;">SingleTile Partial View</span></h2>
<div>
In the following path we have our sinlgetileview partial view ~/Shared/Blocks/Single.Tile.cshtml</div>
<div>
<br /></div>
</div>
<pre style="background: rgb(240, 240, 240); border: 1px dashed rgb(204, 204, 204); font-family: arial; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
@using EPiServer.Reference.Commerce.Site.Core.Settings
@using EPiServer.Reference.Commerce.Site.Infrastructure.Extensions
@model EPiServer.Reference.Commerce.Site.Core.ContentTypes.TemplateCoordinatorInterface.ISingleTileView
<li>
<div class="product-item">
<div class="product-item-image" style="background-image: url(@Html.ResizeImageUrl(Model.ImageUrl, preset: AssetMediaNames.ImageResizerPresets.PRODUCT_RANGE_TILE));">
<img alt="@Model.Title" src="@Html.ResizeImageUrl(Model.ImageUrl, preset: AssetMediaNames.ImageResizerPresets.PRODUCT_RANGE_TILE)" />
</div>
<h2>
@Model.Title</h2>
<div>
@Html.Raw(Model.TileDescription)
</div>
<a class="btn btn-default" href="@Model.LinkToUrl">More info</a>
</div>
</li>
</code>
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-83038183926832320102016-12-03T20:43:00.001+11:002016-12-03T20:43:38.225+11:00Entity Framework Code First Custom Database Initialization and Check if Table Already Exists<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
In one of our Project we had to use existing database and we wanted to use the code first entity framework as well.<br />
<br />
There was requirement to make sure that some of the tables are already existed and all the new ones will be created using the code first approach.<br />
<br />
Therefore, I decided to customized the <b>MigrateDatabaseToLatestVersion.</b><br />
<b><br /></b>
First we will verify that the prerequisite tables are already there and then based on the auto update app setting we will migrate database to latest version.<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">GiftVoucherDatabaseInitialization </span></h2>
</div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
</code>public class GiftVoucherDatabaseInitialization : MigrateDatabaseToLatestVersion<GiftVoucherEntityTypesConfigurationDbContext, Migrations.Configuration>
{
private string IsCodeFirstAutoMigrationEnabled = "IsCodeFirstAutoMigrationEnabled";
public GiftVoucherDatabaseInitialization(string connectionStringName) : base(connectionStringName)
{
}
public override void InitializeDatabase(GiftVoucherEntityTypesConfigurationDbContext context)
{
var queryString = @"
SELECT 1 FROM sys.tables AS T
INNER JOIN sys.schemas AS S ON T.schema_id = S.schema_id
WHERE S.Name = 'dbo' AND T.Name = '{0}'";
if (context.Database.Exists())
{
bool exists = context.Database
.SqlQuery<int?>(string.Format(queryString, "tbl_PurchaseVoucher"))
.SingleOrDefault() != null;
if(!exists)
throw new ContentNotFoundException("Business Foundataion Entity{PurchaseVoucher} Not Found");
exists = context.Database
.SqlQuery<int?>(string.Format(queryString, "tbl_PurchaseVoucherRedemption"))
.SingleOrDefault() != null;
if (!exists)
throw new ContentNotFoundException("Business Foundation Entity{PurchaseVoucherRedemption} Not Found");
}
if (IsMigrationEnabled())
{
base.InitializeDatabase(context);
}
}
private bool IsMigrationEnabled()
{
var migration = ConfigurationManager.AppSettings[IsCodeFirstAutoMigrationEnabled];
return migration != null && migration.ToLower().Equals("true");
}
}
</pre>
<br />
In Global.asax file, we will set the database initializer for above context<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">Setting Up Database Initializer</span></h2>
</div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
</code> protected void Application_Start()
{
// Add the database migration strategy to update the database on app start
Database.SetInitializer(new GiftVoucherDatabaseInitialization(ConnectionStringConfig.CommerceConnectionStringName));
// Run the initializer, but don't force
new GiftVoucherEntityTypesConfigurationDbContext().Database.Initialize(true);
}
</pre>
<br />
<br />
<h2 style="text-align: left;">
<span style="color: red;">GiftVoucherEntityTypesConfigurationDbContext</span></h2>
The GiftVoucherEntityTypesConfigurationDbContext looks like below<br />
<br />
<br /></div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
</code>public class GiftVoucherEntityTypesConfigurationDbContext : DbContext
{
private readonly ICustomerContextFacade _customerContextFacade;
public GiftVoucherEntityTypesConfigurationDbContext()
: this("EcfSqlConnection", null)
{
}
public GiftVoucherEntityTypesConfigurationDbContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public DbSet<PurchaseVoucher> PurchaseVouchers { get; set; }
public DbSet<PurchaseVoucherRedemption> PurchaseVoucherRedemptions { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Dynamically load all configuration
var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => !string.IsNullOrEmpty(type.Namespace))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var type in typesToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}
}
</pre>
<br /></div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0tag:blogger.com,1999:blog-8525160902548525190.post-41723107822463795502016-11-28T19:11:00.001+11:002016-11-29T09:18:10.123+11:00Adding New MetaDataPlus Dictionary Field and Assigning it to Catalog Entry Class<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
<div dir="ltr" style="text-align: left;" trbidi="on">
We will be adding new catalog entry class and will be assigning new meta dictionary field with multiple values.<br />
<div>
<br /></div>
<div>
Adding new Catalog Entry (ProductBase) Class</div>
<div>
<br /></div>
<div>
<br /></div>
</div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[CatalogContentType(GUID = "C39413F3-310C-49C6-A4C4-6B86D6788401", MetaClassName = "ProductBase")]
public class ProductBase : ProductContent
{
[Searchable]
[IncludeInDefaultSearch]
[BackingType(typeof(PropertyDictionarySingle))]
[Display(Order = 60)]
[Required]
public virtual string ProductType { get; set; }
}
</code>
</pre>
</div>
<br />
Now we will be defining initializing class to add new meta field and assign it to Catalog Entry(ProductBase) class<br />
<br />
<br /></div>
<pre style="background: #f0f0f0; border: 1px dashed #cccccc; color: black; font-family: "arial"; font-size: 12px; height: auto; line-height: 20px; overflow: auto; padding: 0px; text-align: left; width: 99%;"><code style="color: black; word-wrap: normal;">
[ModuleDependency(typeof(ServiceContainerInitialization))]
[InitializableModule]
public class CatalogInitialization : IConfigurableModule
{
private bool _isInitialized;
public void Initialize(InitializationEngine context)
{
if (_isInitialized)
return;
SyncMetaClassAndField();
_isInitialized = true;
}
public void Uninitialize(InitializationEngine context)
{
_isInitialized = false;
}
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Container.Configure(ctx => ctx.AddRegistry<shareddependencyregistry>());
}
private static void SyncMetaClassAndField()
{
var metaProClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(MetaDataContext.Instance, "ProductBase");
MetaField metaProp = MetaField.Load(MetaDataContext.Instance, "ProductType");
if (metaProp == null)
{
metaProp = Mediachase.MetaDataPlus.Configurator.MetaField.Create(MetaDataContext.Instance, "Mediachase.Commerce.Catalog", "ProductType", "Product Type", "Product Type Description", MetaDataType.DictionarySingleValue, 1000, false, false, false, false);
}
metaProp.Dictionary.Add("Baby", "Baby");
metaProp.Dictionary.Add("Toddler", "Toddler");
metaProp.Dictionary.Add("Junior", "Junior");
metaProp.Dictionary.Add("Walker", "Walker");
metaProClass.AddField(metaProp);
}
}
</shareddependencyregistry></code></pre>
</div>
Murtaza Dharihttp://www.blogger.com/profile/06979316081987345692noreply@blogger.com0