Blog posts by Vu Ha Bui2021-06-10T07:58:58.0000000Z/blogs/Vu-Ha-Bui/Optimizely WorldDnD support for PropertyList<T>/blogs/Vu-Ha-Bui/Dates/2021/6/dnd-support-for-propertylistt/2021-06-10T07:58:58.0000000Z<p>Hi all,</p>
<p>As you might know then PropertyList<T> is very powerfull. It is help us to easy to manage and organize a list of complex property data but the limitation as I am facing with is that it is not official support sorting via drag and drop (or atleast I don't know how to turn it on - although take hour to googling).</p>
<p><a href="/link/b1b1ac7b1a3746b58bb9e64cb52e8bfb.aspx">Drag and drop not working on product | Episerver Developer Commun</a></p>
<p>Then I digged a bit with javascript solution as below then its worked: (I am assumming that you already know about how to create your module via module.config, create your custom editor and map it to your property list property)</p>
<p>So here is our code: <strong>SortableCollectionEditor</strong>.js</p>
<pre class="language-javascript"><code>define([
// dojo
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/aspect",
"dojo/dom-class",
"dojo/dom-style",
"dojo/topic",
// EPi Framework
"epi/shell/widget/_FocusableMixin",
"epi/shell/dnd/Target",
// epi cms
"epi-cms/contentediting/editors/CollectionEditor",
"epi-cms/contentediting/editors/_TextWithActionLinksMixin",
// epi commerce
"epi-ecf-ui/contentediting/editors/_GridWithDropContainerMixin"
],
function (
//dojo
array,
declare,
lang,
aspect,
domClass,
domStyle,
topic,
// EPi Framework
_FocusableMixin,
Target,
// epi cms
CollectionEditor,
_TextWithActionLinksMixin,
// epi commerce
_GridWithDropContainerMixin,
) {
return declare([CollectionEditor, _GridWithDropContainerMixin, _FocusableMixin], {
// module:
// app/editors/sortablecollectioneditor
// summary:
// Editor widget for support propertylist dnd
resources: { "drophere": "You can drop {content} here", "actions": { "content": "content" } },
postMixInProperties: function () {
// summary:
// Post mix in properties initialization.
// description:
// Fullfills properties that needed to start up the widget.
// tags:
// protected
this.inherited(arguments);
this.gridSettings.dndParams = lang.mixin(this.gridSettings.dndParams, {
_checkAcceptanceForItems: function (items, acceptedTypes) {
var types = array.map(items, function (item) {
return item.data.typeIdentifier;
});
var flag = false;
for (var j = 0; j < types.length; ++j) {
if (types[j] in acceptedTypes) {
flag = !!acceptedTypes[types[j]];
break;
}
}
return flag;
}
});
}
});
});
</code></pre>
<p>Hope that help!</p>
<p>Ha Bui</p>Another Fun and Profit with IMemory caching/blogs/Vu-Ha-Bui/Dates/2020/5/another-fun-and-profit-with-imemory-caching/2020-05-07T20:16:28.0000000Z<p>Hi all,</p>
<p>As you might know Quan Mai already had a great post: <a href="/link/ccbaba8537dc44c9816b48df2f6684d9.aspx">https://world.episerver.com/blogs/Quan-Mai/Dates/2019/12/use-memorycache-in-your-project---for-fun-and-profit/</a></p>
<p>Today I want to introduce other way base on </p>
<h1>Microsoft.Extensions.Caching.Memory</h1>
<p>Some references for you:</p>
<p>1. <a href="https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/">https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/</a></p>
<p>2. <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache?view=dotnet-plat-ext-3.1">https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache?view=dotnet-plat-ext-3.1</a></p>
<p>If you make a search with "IMemory" then quite a lot of tutorial but almost is in .Net Core but doesn't blocking us to apply on .NET and EPiServer as well!</p>
<p>Okay, lets go step by step!</p>
<ol>
<li>Create your interceptor class: <strong>MemoryCacheInterceptor</strong><strong></strong></li>
<li>Intercep via ConfigurationModule and StructureMap interceptors pattern: <strong>MemoryCacheConfigurationModule</strong><strong></strong></li>
</ol>
<pre class="language-csharp"><code>[InitializableModule]
[ModuleDependency(typeof(FrameworkInitialization))]
public class MemoryCacheConfigurationModule : IConfigurableModule, IInitializableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.ConfigurationComplete += (o, e) =>
{
e.Services.Intercept<IObjectInstanceCache>((locator, httpRuntimeCache) =>
{
return new MemoryCacheInterceptor();
});
};
}
public void Initialize(InitializationEngine context)
{
}
public void Uninitialize(InitializationEngine context)
{
}
}
/// <summary>
/// Implement IObjectInstanceCache with IMemoryCache
/// <see cref="Microsoft.Extensions.Caching.Memory.IMemoryCache"/>
/// </summary>
public class MemoryCacheInterceptor : IObjectInstanceCache, IDisposable
{
// Interesting things will come here
}</code></pre>
<p>Our main focus will be <strong>MemoryCacheInterceptor </strong>class, as you see its inherited from <strong>IObjectInstanceCache </strong>and <strong>IDisposable</strong> then we should implement those methods below:</p>
<p>0. <strong>Properties</strong> and <strong>Constructor</strong></p>
<pre class="language-csharp"><code>private static readonly ILog _log = LogManager.GetLogger(typeof(MemoryCacheInterceptor));
private readonly IMemoryCache _memoryCache;
private readonly object _dependencyObject;
private CancellationTokenSource _rootTokenSource;
private readonly ConcurrentDictionary<string, CancellationTokenSource> _tokenSources;
public MemoryCacheInterceptor()
{
_memoryCache = new MemoryCache(new MemoryCacheOptions());
_dependencyObject = new object();
_rootTokenSource = new CancellationTokenSource();
_tokenSources = new ConcurrentDictionary<string, CancellationTokenSource>();
_log.Info("Started NitecoMemeoryCacheInterceptor");
}</code></pre>
<p>As you see:</p>
<p><strong>_memoryCache </strong>: Create new instance of <strong>MemoryCache </strong>with posibility of customize option on <strong>MemoryCacheOptions </strong>like how much memory will be used ... See more in: <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions?view=dotnet-plat-ext-3.1">https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions?view=dotnet-plat-ext-3.1</a></p>
<p><strong>_dependencyObject </strong>: Dummy object cache of all master keys.</p>
<p><strong>_rootTokenSource </strong>that is <strong>IMemory</strong> techinique based on <strong>CancellationToken </strong>that will help us to <span>to invalidate a set of cache all in one go. You will see relations betweeen a <strong>CacheEntry</strong> and the token later on <strong>Insert</strong> method!</span> </p>
<p><strong>_tokenSources </strong>: Tokens provider for each cache key</p>
<p>1. public void <strong>Clear</strong>()</p>
<pre class="language-csharp"><code>public void Clear()
{
if (_rootTokenSource != null && !_rootTokenSource.IsCancellationRequested && _rootTokenSource.Token.CanBeCanceled)
{
_rootTokenSource.Cancel();
_rootTokenSource.Dispose();
}
_rootTokenSource = new CancellationTokenSource();
}</code></pre>
<p>2. public object <strong>Get</strong>(string key)</p>
<pre class="language-csharp"><code>public object Get(string key)
{
return _memoryCache.Get(key);
}</code></pre>
<p>3. public void <strong>Remove</strong>(string key)</p>
<pre class="language-csharp"><code>public void Remove(string key)
{
_memoryCache.Remove(key);
}</code></pre>
<p>4. public void <strong>Dispose</strong>()</p>
<pre class="language-csharp"><code>public void Dispose()
{
_memoryCache.Dispose();
}</code></pre>
<p>5. public void <strong>Insert</strong>(string key, object value, CacheEvictionPolicy evictionPolicy)</p>
<p>That is main part and we should take a coffee then focusing on ;)</p>
<p>...</p>
<pre class="language-csharp"><code>public void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)
{
if (evictionPolicy == null)
{
// depend on root token only
_memoryCache.Set(key, value, new CancellationChangeToken(_rootTokenSource.Token));
return;
}
// Try setup dummy master object cache.
EnsureMasterKeys(evictionPolicy.MasterKeys);
using (var cacheEntry = _memoryCache.CreateEntry(key))
{
// Propagate tokens to current cache entry.
AddDependencyTokens(evictionPolicy);
var cacheEntryOption = GetCachEntryOption(key, evictionPolicy);
cacheEntry.SetOptions(cacheEntryOption.AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token)));
cacheEntry.SetValue(value);
}
}</code></pre>
<p>Okay are you ready?</p>
<p>Simple part in the function is: Check null of <strong>evictionPolicy</strong> then insert to memory but remember to tight this cache key on our root token source to eviction all cache on <strong>Clear</strong> method! Done!</p>
<pre class="language-csharp"><code>if (evictionPolicy == null)
{
// depend on root token only
_memoryCache.Set(key, value, new CancellationChangeToken(_rootTokenSource.Token));
return;
}</code></pre>
<p>Next we ensure master keys are ready to use (have cached object) with <strong>EnsureMasterKeys</strong>(evictionPolicy.MasterKeys) method:</p>
<pre class="language-csharp"><code>private void EnsureMasterKeys(string[] masterKeys)
{
if (masterKeys != null)
{
foreach (string key in masterKeys)
{
object cached;
if (!_memoryCache.TryGetValue(key, out cached))
{
var token = _tokenSources.GetOrAdd(key, new CancellationTokenSource());
_memoryCache.Set(key, _dependencyObject,
new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTimeOffset.MaxValue)
.SetPriority(CacheItemPriority.NeverRemove)
.AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token))
.AddExpirationToken(new CancellationChangeToken(token.Token))
.RegisterPostEvictionCallback(PostEvictionCallback));
}
}
}
}</code></pre>
<p>Our idea here is: Loop through master kesy, if it's not existed then insert with an absoluted maximum time and highest cache priority so in theory it will never been automactically removed by Garbage Collection (GC). Then create token source for it, add the token and root token source together to the cache entry option.</p>
<p>Oh but <strong>what</strong> and <strong>why</strong> do we need <strong>PostEvictionCallback </strong>? and <strong>when</strong> it will be trigerred? </p>
<p><strong>What </strong>and <strong>When</strong>:<span>The given callback will be fired after the cache entry is evicted from the cache. </span> <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.cacheentryextensions.registerpostevictioncallback?view=dotnet-plat-ext-3.1">Read more</a></p>
<p><strong>Why </strong>: Whenever the master cache is evicted then we should remove all children depend on the master token!</p>
<pre class="language-csharp"><code>private void PostEvictionCallback(object key, object value, EvictionReason evictionReason, object state)
{
CancellationTokenSource token;
if (_tokenSources.TryRemove((string)key, out token)
&& token != null && !token.IsCancellationRequested && token.Token.CanBeCanceled)
{
token.Cancel();
token.Dispose();
}
}</code></pre>
<p>Last part: Create new cache entry, add dependency tokens and of course with root token as well and then set to memory!</p>
<pre class="language-csharp"><code>using (var cacheEntry = _memoryCache.CreateEntry(key))
{
// Propagate tokens to current cache entry.
AddDependencyTokens(evictionPolicy);
var cacheEntryOption = GetCachEntryOption(key, evictionPolicy);
cacheEntry.SetOptions(cacheEntryOption.AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token)));
cacheEntry.SetValue(value);
}</code></pre>
<p>The magic part <strong>AddDependencyTokens</strong>(evictionPolicy)</p>
<pre class="language-csharp"><code>private void AddDependencyTokens(CacheEvictionPolicy evictionPolicy)
{
var dependencies = evictionPolicy.CacheKeys;
if (dependencies == null)
{
dependencies = evictionPolicy.MasterKeys;
}
else if (evictionPolicy.MasterKeys != null)
{
dependencies = dependencies.Concat(evictionPolicy.MasterKeys).ToArray();
}
if (dependencies == null) return;
foreach (var k in dependencies)
{
object v;
_memoryCache.TryGetValue(k, out v);
}
}</code></pre>
<p>Hmm, quite strange right? Firstly, combination cache keys with master keys (we're considering cache keys and master keys are dependencies) and then just do: <strong>TryGetValue </strong>? Is that enough? Yes, because of when you use:</p>
<pre class="language-csharp"><code>using (var cacheEntry = _memoryCache.CreateEntry(key))</code></pre>
<p>then you put this entry on the top of scope, see more: <strong>Microsoft.Extensions.Caching.Memory.CacheEntry.CacheEntry</strong></p>
<pre class="language-csharp"><code>internal CacheEntry(object key, Action<CacheEntry> notifyCacheEntryDisposed, Action<CacheEntry> notifyCacheOfExpiration)
{
...
_scope = CacheEntryHelper.EnterScope(this);
}</code></pre>
<p>and <strong>Microsoft.Extensions.Caching.Memory.MemoryCache.TryGetValue</strong></p>
<pre class="language-csharp"><code>public bool TryGetValue(object key, out object result)
{
...
value.PropagateOptions(CacheEntryHelper.Current);
...
}</code></pre>
<p>Next is simple but important function <strong>GetCachEntryOption </strong></p>
<pre class="language-csharp"><code>private MemoryCacheEntryOptions GetCachEntryOption(string key, CacheEvictionPolicy evictionPolicy)
{
var cacheEntryOption = new MemoryCacheEntryOptions();
switch (evictionPolicy.TimeoutType)
{
case CacheTimeoutType.Undefined:
break;
case CacheTimeoutType.Sliding:
cacheEntryOption = cacheEntryOption.SetSlidingExpiration(evictionPolicy.Expiration);
break;
case CacheTimeoutType.Absolute:
cacheEntryOption = cacheEntryOption.SetAbsoluteExpiration(evictionPolicy.Expiration);
break;
}
var tokenSource = _tokenSources.GetOrAdd(key, new CancellationTokenSource());
return cacheEntryOption
.AddExpirationToken(new CancellationChangeToken(tokenSource.Token))
.RegisterPostEvictionCallback(PostEvictionCallback);
}</code></pre>
<p>Once againe you see <strong>PostEvictionCallback </strong>here with the same logic: When cache is evicted then we will cancel the token so that it will evict all others dependent cache entries!</p>
<p>Viola! Happy Coding!</p>Export content with versions/blogs/Vu-Ha-Bui/Dates/2020/1/export-content-with-versions/2020-01-21T15:20:21.0000000Z<p>Hi guys,</p>
<p>As you knows, currently, EPiServer just supports latest published version in languages when you uses the default admin tool (Export / Import Data).</p>
<p>Of course in most case, this is enough but sometimes you need to export / import with versions as well. (at least in case editor created a common draft and doesn't want to rework)</p>
<p>How can we archive that? The solution (of-course by default EPiServer doesn't support it then you also should accepts some tips and tricks in code). No worry so much because of we still base on EPiServer default one quite a lot)</p>
<p>Below is our solution:</p>
<ol>
<li>Use structure map interceptor to apply our tips and tricks via InitializeModule</li>
<li>Intercep on <strong>DefaultDataExporter</strong> to export contents with versions (instead of only latest published one)</li>
<li>Intercep on <strong>DefaultContentImporter </strong>to import content version</li>
</ol>
<p>Some small things should be considered like:</p>
<p>+ Keep saved date</p>
<p>+ Keep saved by (require user migrating)</p>
<p>Okay, lets start steps by steps:</p>
<h3>1. Use structure map interceptor to apply our tips and tricks via InitializeModule</h3>
<p>Your code should looks like this:</p>
<pre class="language-csharp"><code>[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class DataExporterInitializationModule : IConfigurableModule, IInitializableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.ConfigurationComplete += (o, e) =>
{
// Your interceptor logic here
e.Services.Intercept<IDataExporter>((locator, defaultDataExporter) =>
...);
e.Services.Intercept<IContentImporter>((locator, defaultDataImporter) =>
...);
}
}</code></pre>
<h3>2. Intercep on <strong>DefaultDataExporter</strong> to export contents with versions (instead of only latest published one)</h3>
<p>We will override <strong><em>ExportContent</em> </strong>method, check our new option (ExportVersion) or just keep default one:</p>
<pre class="language-csharp"><code>protected override void ExportContent(XmlTextWriter xml
, ContentReference contentToExport
, IContentTransferContext context
, ITransferContentData transferContent) {
if (_transferExportOptionsEx.ExportVersion) {
ExportContentWithVerion(xml, contentToExport, context, transferContent, base.ExportContent);
}
else {
base.ExportContent(xml, contentToExport, context, transferContent);
}
}</code></pre>
<p>Then get all versions and build raw transfer content data via:</p>
<pre class="language-csharp"><code>protected virtual TransferContentData BuildRawTransferContent(IContentTransferContext context
, List<ContentLanguageSetting> contentLanguageSettings
, IRawContentRetrieverEx rawContentRetieverEx, IContent version)
{
var transferVersionContent = new TransferContentData()
{
RawContentData = rawContentRetieverEx.CreateRawContent(version)
};
if (contentLanguageSettings != null)
transferVersionContent.ContentLanguageSettings = contentLanguageSettings;
PropertyExportContext propertyExportContext = new PropertyExportContext
{
TransferContext = context,
TransferOptions = Options,
Output = transferVersionContent.RawContentData,
Source = version
};
_propertyExporter.ExportProperties(version
, transferVersionContent.RawContentData.Property
, propertyExportContext);
return transferVersionContent;
}</code></pre>
<p>Viola! You have just done a big task! Export Content With Version task!</p>
<p>The last one is import the exported content versions (keep version status, keep saved date and saved by)! Take a coffee and relax before we go to the rest!</p>
<p>... (coffee break)</p>
<h3>3. Intercep on <strong>DefaultContentImporter </strong>to import content version</h3>
<p><em>This one is hard part and get much of your pains :( Remember do testings carefully. No pains no gains right :))</em></p>
<p><em>We should override IContent Import method (<strong>protected override IContent Import</strong>) with some tricks as below:</em></p>
<h4>3.1 Tricks to keep language content version: (because of our exported versions are mixing with multiple languages and those versions are flatten)</h4>
<pre class="language-csharp"><code>CultureInfo originalSelectedLanguage = null;
if (importedContentData.SelectedLanguage != null)
originalSelectedLanguage = CultureInfo.GetCultureInfo(importedContentData.SelectedLanguage.Name);
var selectedLanguageAction = importedContentData.GetType().GetProperty("SelectedLanguage");
var contentLanguage = importedContentData.GetLanguageBranch();
if (!string.IsNullOrEmpty(contentLanguage))
{
selectedLanguageAction.SetValue(importedContentData, CultureInfo.GetCultureInfo(contentLanguage));
}</code></pre>
<h4>3.2 Tricks to keep content versions status (because of EPiServer just "accept" publish action OMG, other actions just for new one):</h4>
<pre class="language-csharp"><code>var status = importedContentData.GetStatus();
var propSaveAction = context.GetType().GetProperty("SaveAction");
SaveAction originalSaveActions = SaveAction.Publish | SaveAction.SkipValidation;
if (!string.IsNullOrEmpty(status) && int.Parse(status) < (int)VersionStatus.Published)
{
propSaveAction.SetValue(context, SaveAction.CheckOut | SaveAction.ForceNewVersion | SaveAction.SkipValidation);
}</code></pre>
<h4>3.3 Small things to keep saved date and saved by</h4>
<pre class="language-csharp"><code>ContextCache.Current["PageSaveDB:ChangedBy"] = string.Empty;
ContextCache.Current["PageSaveDB:PageSaved"] = string.Empty;</code></pre>
<p>Okay, last one is: call to base method:</p>
<pre class="language-csharp"><code>base.Import(importedContentData
, requiredDestinationAccess
, context, options, out importedPageGuid);</code></pre>
<p>That is all ? Not yet, something you should deal with:</p>
<p>+ In your interceptor class of DefaultContentImporter, please consider to save original data and then set it back right after content version is imported to avoid any impact.</p>
<p>+ EPiServer DefaultContentImporter only save your version if it's the new one :(</p>
<p>We can resolve those things like this:</p>
<pre class="language-csharp"><code>try
{
// check the current content version is new or not
var importedContentGuid = new Guid(importedContentData.GetContentGuid());
var handleContentGuidMethod = defaultContentImporter.GetType()
.GetMethod("HandleContentGuid", BindingFlags.NonPublic | BindingFlags.Instance);
var guid = (Guid)handleContentGuidMethod.Invoke(
defaultContentImporter
, new object[] { importedContentGuid, context });
PermanentLinkMap permanentLinkMap = permanentLinkMapper.Find(guid);
var baseContent = base.Import(importedContentData
, requiredDestinationAccess
, context, options, out importedPageGuid);
if (permanentLinkMap != null && (context.SaveAction & SaveAction.Publish) != SaveAction.Publish)
contentRepository.Save(baseContent, context.SaveAction, requiredDestinationAccess);
return baseContent;
}
catch (Exception ex)
{
Logger.Error(ex);
throw;
}
finally
{
selectedLanguageAction.SetValue(importedContentData, originalSelectedLanguage);
propSaveAction.SetValue(context, originalSaveActions);
ContextCache.Current["PageSaveDB:ChangedBy"] = orgPageSaveDBChangeBy;
ContextCache.Current["PageSaveDB:PageSaved"] = orgPageSaveDBPageSaved;
}</code></pre>
<p>Congratulations! Now please try it by your self with AlloyMvc template! Below is my result:</p>
<p><img src="/link/9d0df7c3362445cbb237e503472dc0d9.aspx" /></p>
<p>Full source code can be found under: <a href="https://github.com/NitecoOPS/ExportImportWithVersion">https://github.com/NitecoOPS/ExportImportWithVersion</a></p>
<p>Hope that the article will help to reduce your headache! ;)</p>
<p>---</p>
<p>Happy Coding!</p>
<p>.HaBui</p>Get rid of EPiServer Commerce Migrate redirect loop/blogs/Vu-Ha-Bui/Dates/2019/9/get-rid-episerver-commerce-migrate-redirect-loop/2019-10-01T00:02:48.0000000Z<p>Phew! Take 2 hours to get rids of the infinitive loop from EPiServer ECommerce after upgrade from 9.2 to 12.17!</p>
<p>You can Google search with: <em>episerver commerce migration loop</em></p>
<p>Then some guys below appear:</p>
<p>1. <a href="/link/71876581bd08425998854838c93ae25c.aspx">https://world.episerver.com/forum/developer-forum/Episerver-Commerce/Thread-Container/2019/5/passing-thru-episervercommercemigrate-when-created-new-site-in-azure/</a></p>
<p>2. <a href="/link/13ffd45f454440e9a2c97c9db0a06f27.aspx">https://world.episerver.com/forum/developer-forum/Episerver-Commerce/Thread-Container/2016/10/too-many-redirects-error-in-the-browser-after-package-update/</a></p>
<p>Some good instructions but does not work for my case: MigrateRedirect -> <RequireLogin> -> Owin -> MigrateRedirect -> <RequireLogin> -> ...</p>
<p>You can see more in those class:</p>
<p>+ <span>EPiServer.Commerce.Internal.Migration.MigrationInitializationModule</span></p>
<p>+ <span>EPiServer.Commerce.Internal.Migration.MigrationManager</span></p>
<p>...</p>
<p>Okay, so how can we resolve this? Some angeles below will help:</p>
<p>1. <span>// EPiServer.Commerce.Internal.Migration.MigrationManager</span></p>
<p>public virtual void MigrateAsync()</p>
<p>2. IoC with StructureMap and Interceptors holy light</p>
<pre class="language-csharp"><code>[InitializableModule]
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
[ModuleDependency(typeof(CmsCoreInitialization))]
internal class ContainerInitialization : IConfigurableModule, IInitializableModule</code></pre>
<p>and</p>
<pre class="language-csharp"><code>public void ConfigureContainer(ServiceConfigurationContext context)
{
...
context.ConfigurationComplete += (o, e) =>
{
e.Services.Intercept<MigrationManager>((locator, defaultMigrationManager) =>
new MigrationManagerInterceptor(defaultMigrationManager
, locator.GetInstance<MigrationStore>()
, locator.GetInstance<MigrateActionUrlResolver>()));
};
}</code></pre>
<p>and last one:</p>
<pre class="language-csharp"><code>public class MigrationManagerInterceptor : MigrationManager
{
private MigrationManager _defaultMigrationManager;
public MigrationManagerInterceptor(
MigrationManager defaultMigrationManager
, MigrationStore migrationStore
, MigrateActionUrlResolver migrateActionUrlResolver)
: base(migrationStore, migrateActionUrlResolver)
{
_defaultMigrationManager = defaultMigrationManager;
}
public override void RedirectToMigrationView()
{
MigrateAsync();
return;
}
}</code></pre>
<p>Good bye redirect loop! Cheer!</p>
<p>// Ha Bui</p>EPiServer Addons Development - VisualStudio Extensions/blogs/Vu-Ha-Bui/Dates/2019/8/episerver-addons-development---visualstudio-extensions/2019-08-28T15:45:59.0000000Z<p>Good day everyone,</p>
<p>As you know that, split your solution to multiple project and packaged as nuget is common way for now.</p>
<p>That will help us to isolate, idependent and better for deployment / maintaince. EPiServer Add-ons also follow this and as you see, so many helpfull add-on packages are available for you! (Of course, continue or dis-continue maintained, upgrade version to adapt with many EPiServer version is a long story).</p>
<p>Today I just focus on : The way (nuget) for developer. How to help developer is easier create their package?</p>
<p>From this view point I created an visual studio extesion that is packaged my protected add-on template (public is the same with very small change, but I think we should have it in near future!).</p>
<p>Some screenshots for you:</p>
<p>Add new add-on project:</p>
<p><img src="/link/4c7d04bdcf0542acb92673b5d715433a.aspx" width="578" height="205" /></p>
<p>select template and:</p>
<p><img src="/link/0328ca2b438e449fb06f1a1f5b62733c.aspx" width="679" height="458" /></p>
<p>Click [Create] and you will see add-on project structure:</p>
<p><img src="/link/a1a12e2956894ec78784fb61cf92652a.aspx" width="687" height="311" /></p>
<p>Build and you will see the package in: LocalPackage folder under your solution root folder ($(SolutionDir)LocalPackages) that is my harded code that will help you if your solution added local repository point to this folder! (You can change to another folder as you wish)</p>
<p><img src="/link/8b445dfa26034e9bbdc956b69858d64a.aspx" width="777" height="124" /></p>
<p>
<p>Nice? :) (clap clap clap) 0:)</p>
<p>---</p>
<p>Happy Coding! (Source code and extension binary will availabel soon in comments ha ha ha)</p>
<p>Ha Bui</p>
</p>EPiServer Nature Language (wrap up base on Google NLP and Budou)/blogs/Vu-Ha-Bui/Dates/2019/8/episerver-nature-language-wrap-up-base-on-google-nlp-and-budou/2019-08-22T19:54:01.0000000Z<p>Hi all,</p>
<p>
<p>If nothing change then tomorrow is Friday (oh my gods, this is weekend!).</p>
<p>So firstly, I want to say happy weekend to everyone! Later on, I want to share my exprerience about Nature Language Processing ...</p>
<p>Hey! Don't think about AI or decesion tree ... No no, just a small C# port for:</p>
<ol>
<li>Budou (<a href="https://github.com/google/budou">https://github.com/google/budou</a>)</li>
<li>Google NLP API (<a href="https://cloud.google.com/natural-language/">https://cloud.google.com/natural-language/</a>)</li>
</ol>
<p>And wrap all under DB storage (EF custom table) and of course is great caching layer of EPiServer (<a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userid=748045d9-fea9-db11-8952-0018717a8c82">Johan Björnfot</a>: <a href="/link/507bd0a091f24692aec6f5c8bfbf3f36.aspx">https://world.episerver.com/blogs/Johan-Bjornfot/Dates1/2018/5/iobjectinstancecache-readthrough/</a>) (<em>will available soon via nuget package</em>)</p>
<p>---</p>
<p>It's for someone want to have better words wrap for CJK (China, Japanese, Korean) languages on your website.</p>
<ul>
<li>Step 1: Register Google NLP</li>
<li>Step 2: Export credential </li>
<li>Step 3: Play with my simple project (cmd) in <a href="https://github.com/NitecoOPS/BudouCSharp">https://github.com/NitecoOPS/BudouCSharp</a>.</li>
</ul>
<p><em>P.S: Nuget link will be update soon for you :) (could we have some comments to encorage me?)</em></p></p>Custom model for LinkItemCollection property/blogs/Vu-Ha-Bui/Dates/2018/4/custom-model-for-linkitemcollection-property/2018-04-13T13:09:08.0000000Z<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>In many CMS project, we have many requirements that use LinkItemCollection property but with customize model (default is LinkItem).</p>
<p>Below is the way we do:</p>
<ol>
<li>Define property with type <strong>LinkItemColleciton</strong> in your content type like this:</li>
</ol>
<p><img src="/link/cd3b943f1d1e41feb5f8f1377c6ce270.aspx" alt="Image Step1_Add_Property.JPG" /></p>
<p>2. Add new EditorDescriptor like this:</p>
<p><img src="/link/0b77ab9a231f415cb8149f1ec3b32a66.aspx" alt="Image Step2_Add_Custom_Editor_Descriptor.JPG" /></p>
<p>3. Add new .js file for your custom model like this (match with the path in step 2):</p>
<p><img src="/link/59bdc6c435b546c6bf53b28ca00061a8.aspx" alt="Image Step3_Add_Custom_Item_Model.JPG" /></p>
<p>3. The last step is <strong>HACK</strong> way, because of item collection editor need the model above injected from the beginning (@EPiServer guy: should be improved by lazy load !?)</p>
<p><img src="/link/11df267ba7fd49aaa898937f110316a9.aspx" alt="Image Last_Step.JPG" /></p>
<p>That all!</p>
</body>
</html>Duplicate language versions after import/blogs/Vu-Ha-Bui/Dates/2018/4/duplicate-language-versions-after-import/2018-04-12T05:20:28.0000000Z<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>When I work with EPiSerser Commerce's import catalog I have an issue after import catalog successfully like:</p>
<p><img src="/link/cd95181496944b1fb2f1c0063d2f008f.aspx" alt="Image issue1.JPG" /></p>
<p>After 3 hours investigate I found down that is by:</p>
<p>Default catalog language is: en<strong>-</strong>gb but the available languages are: en-<strong>GB</strong>. That because of this EPiSErver function <em>VerifyLanguageSetup </em>also call <em>Distinct </em>but without <em>CurrentCultureIgnoreCase.</em></p>
</body>
</html>