Blog posts by stackmanoz2023-02-27T15:09:58.0000000Z/blogs/stackmanoz/Optimizely WorldDeleting missing metafields in code programmatically/blogs/stackmanoz/dates/2023/2/deleting-missing-metafields-in-code-programmatically/2023-02-27T15:09:58.0000000Z<p>This blog post will help you in deleting meta fields programatically in catalogue context. We will only delete metafields which are user generated and missing from code (deleted later in the code) but remain in database. </p>
<p>#1 - get the list of metaclasses (user generated) </p>
<pre class="language-csharp"><code> private readonly Mediachase.MetaDataPlus.Configurator.MetaClassCollection _metaClassCollection;
_metaClassCollection = Mediachase.MetaDataPlus.Configurator.MetaClass.GetList(CatalogContext.MetaDataContext, true);
_metaClassCollection.Cast<Mediachase.MetaDataPlus.Configurator.MetaClass>()
.Where(c => c.IsUser
&& c.MetaFields.Any(x => x.IsUser))</code></pre>
<p>The above collection will fetch meta classes created by the user and any of it's metafields is user generated at least. </p>
<p>#2 - Getting metafields for class</p>
<pre class="language-csharp"><code> metaClass.MetaFields
.Where(c => c.IsUser && c.OwnerMetaClassIdList.MetaCollectionHasOnly(id))</code></pre>
<p>Above code snippet selects all the meta fields for a metaclass and makes sure it is not fetching any other metafield under a class which comes from relation of the same class (i.e. product > Category > Catalog)<br />Therefore, It only displays meta fields strictly which are part of the class. I've used an extension method that makes sure each metafiled has only 1 exact same parent as current class. <br /><br /></p>
<pre class="language-csharp"><code> public static bool MetaCollectionHasOnly(this MetaClassIdCollection collection, int classId)
{
try
{
if (collection != null && collection.Count == 1)
{
return collection[0] == classId;
}
return false;
}
catch
{
//we don't want to catch exception in this case.
return false;
}
}</code></pre>
<p>Now it comes to the final step which is most important - Identify missing field from class</p>
<p>Each metaclass is mapped with contentType class you have in your code. Here we need to figure out which class in code is mapped with metaclass in database. Let's understand it easy way -</p>
<p>If I have a base class for all the products in the code as <strong>ProductBase</strong> with <strong>[CatalogContentType]</strong> decorator then for MetaClass <strong>Product</strong> MetaClassToContentTypeMap will fetch <strong>ProductBase.</strong></p>
<p>Now the only thing you need to check if the metafield exists in the code as follows - </p>
<pre class="language-csharp"><code>private readonly Injected<MetaClassToContentTypeMap> _contentTypeModelRepository;
public string MetaFieldName { get; set; }
private Type ClassType
{
get
{
return _contentTypeModelRepository.Service.GetContentTypeModel(MetaClassId);
}
}
public bool IsAvailable
{
get
{
return ClassType.HasProperty(this.MetaFieldName);
}
}</code></pre>
<p><strong>IsAvailable </strong>property returns whether the metafield exists in the code or not. </p>
<p>--------------------------------------------------------------</p>
<p>Update - Deleting what we have detected so far. In my project I had an endpoint which would do it for me. The field you want to delete, send it along with it's parent classId. </p>
<pre class="language-csharp"><code> [HttpPost]
[Route("metafields/{classId}/delete/{fieldId}", Name = "DeleteMetaField")]
public JsonResult Delete(int classId, int fieldId)
{
var result = false;
try
{
var metaClass = _metaClassCollection.Cast<Mediachase.MetaDataPlus.Configurator.MetaClass>()
.FirstOrDefault(c => c.Id.Equals(classId));
if (metaClass != null)
{
var metaField = metaClass.MetaFields.FirstOrDefault(c => c.Id.Equals(fieldId));
metaField.Delete();
}
result = true;
}
catch (Exception ex)
{
Log.Error("Failed to delete" + ex.Message);
result = false;
}
return new JsonResult(result);
}</code></pre>
<p>And the extension method to delete: </p>
<pre class="language-csharp"><code>public static class MetaFieldExtensions
{
private static readonly MetaDataContext Context = CatalogContext.MetaDataContext;
public static void Delete(this MetaField metaField)
{
if (metaField == null)
{
return;
}
foreach (var metaClassId in metaField.OwnerMetaClassIdList)
{
var cls = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(Context, (int)metaClassId);
cls.DeleteField(metaField);
}
MetaField.Delete(Context, metaField.Id);
}
}</code></pre>
<p>Hope this helps. </p>Working with product recommendations - Wishlist Recommendations/blogs/stackmanoz/dates/2022/5/working-with-product-recommendations/2022-06-01T11:13:42.0000000Z<p>One of the useful features of Optmizely personalized contents is Product Recommendations. <span>With Optimizely </span><span class="VariablesPerform">Product Recommendations</span><span>, you can provide a personalized shopping experience for visitors to your e-commerce website. </span><span>Personalization is based on website interaction such as order history, visitor profiles, and intelligent algorithms to suggest products of interest. A developer must first configure the tracking, personalization service, and recommendation widgets, then you can start working with </span><span class="VariablesPerform">Product Recommendations</span><span> to define recommendations strategies using the </span><span class="Variablesportal">Personalization Portal</span><span>. </span></p>
<p>A couple of days ago we integrated a widget for wishlist, Where users would see product recommendation based on their wishlist history. Below are the steps how you can do it step by step. This is the same configuration you can follow to create other widgets. </p>
<p><strong>Step 1</strong> - Login to Personalization portal > Product Recommendations > Widgets > Create a new widget<br />For us this was <em>wishlistWidget</em> we needed on wishlist page. </p>
<p><strong>Step 2 </strong>- Configure the type of recommendations you would like to display on your website <br />The algorithms are set to show <em>Popular only 3 <strong> </strong></em>recommendations. That way it would show us only 3 product recommendations.<img src="https://episerver.zendesk.com/attachments/token/StTlNPH5kz0vyLcD9JInnL4Fb/?name=mceclip0.png" width="623" height="310" /></p>
<p><strong>Code > </strong>Wishlist > enable tracking </p>
<pre class="language-csharp"><code>private readonly TrackingDataFactory _trackingDataFactory;
private readonly ITrackingService _trackingService;
private readonly ServiceAccessor<IContentRouteHelper> _contentRouteHelperAccessor;
public async Task<TrackingResponseData> TrackWishlist(HttpContextBase httpContext)
{
var trackingData = _trackingDataFactory.CreateWishListTrackingData(httpContext);
return await InternalTrackAsync(trackingData, httpContext);
}
private async Task<TrackingResponseData> InternalTrackAsync(CommerceTrackingData commerceTrackingData, HttpContextBase httpContext)
{
if (commerceTrackingData == null || httpContext == null)
{
return null;
}
var scope = _trackingDataFactory.GetCurrentTrackingScope();
if (!string.IsNullOrEmpty(scope))
{
return await _trackingService.TrackAsync(commerceTrackingData, httpContext, _contentRouteHelperAccessor().Content, scope);
}
else
{
return await _trackingService.TrackAsync(commerceTrackingData, httpContext, _contentRouteHelperAccessor().Content);
}
}</code></pre>
<p>Next > Get recommendations</p>
<pre class="language-csharp"><code>using EPiServer.Personalization.Commerce.Tracking;
public static IEnumerable<Recommendation> GetRecommendations(this TrackingResponseData response, ReferenceConverter referenceConverter, string area) => response.GetRecommendationGroups(referenceConverter)
.Where(x => (!string.IsNullOrEmpty(area) && x.Area.IsEqual(area))
|| string.IsNullOrEmpty(area))
.SelectMany(x => x.Recommendations);</code></pre>
<p>This will fetch you configured recommendations on personlization portal. Similarly we can get recommendations wherever required. </p>
<p>Here is the useful resource for enabling tracking for wishlist in commerce - <a href="https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Cart/Controllers/WishListController.cs">https://github.com/episerver/Quicksilver/blob/master/Sources/EPiServer.Reference.Commerce.Site/Features/Cart/Controllers/WishListController.cs</a> </p>
<p>Thank you for reading.</p>Updated commands to install Commerce14 and CMS12/blogs/stackmanoz/dates/2022/3/updated-commands-to-install-commerce14-and-cms12/2022-03-21T14:54:30.0000000Z<p>This is the most recent exception that I got while installing Commerce 14 on local. I followed following commands to install it - </p>
<pre class="language-csharp"><code>dotnet-episerver create-cms-database FirstCore.csproj -S . -E
dotnet-episerver create-commerce-database FirstCore.csproj -S . -E --reuse-cms-user
dotnet-episerver add-admin-user FirstCore.csproj -u username -p password -e user@email.com -c EcfSqlConnection</code></pre>
<p>I had setup everything but databases were empty with following exception </p>
<p><img src="/link/c674f7f3cd634289a1eab8b583d7a87d.aspx" /></p>
<p>Both the databases were empty (CMS, Commerce) and no schemas were present. I opened a forum request <a href="/link/2f49b66985d4404990f3a4b9e1261f5f.aspx">here</a> and looks like the current documentation is outdated. </p>
<p>For making it work with old commands there is an additional command that you follow and specify .NET standard version - </p>
<pre class="language-csharp"><code>dotnet add "[PROJECTNAME].csproj" package System.Configuration.ConfigurationManager -v 5.0.0
</code></pre>
<p><strong>New commands with Optimizely Templates</strong></p>
<pre class="language-csharp"><code>dotnet new -i Episerver.Templates
dotnet new epi-commerce-empty
dotnet new epi-cms-empty
dotnet run</code></pre>
<p>Credits to <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=94b14ae6-07fa-db11-94ce-0018717a8c82">Mari </a>and <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=9b7baa52-572a-e011-915e-0018717a8c82">Chris</a></p>
Prevent content events from the loop/blogs/stackmanoz/dates/2022/3/stopping-the-loop-of-content-events/2022-03-08T10:14:12.0000000Z<p>I'm writing a simpler solution to the problem that most of us keep getting while working with ContentEvents. There might be solution to this already but at least I couldn't find it easily. </p>
<p>I was working on content events where on PublishedContent I had to refer products under the category. Now this needs <a href="/link/a0f73c83f9874dd5aeec4bf69739c27c.aspx">ContentEvents </a></p>
<pre class="language-csharp"><code>_contentEvents.PublishedContent += ContentEvents_PublishedContent;
private static void ContentEvents_PublishedContent(object sender, ContentEventArgs e)
{
SaveDynamicCategory(e.Content);
}
</code></pre>
<p>The method above SaveDynamicCategory has a save method as below - </p>
<pre class="language-csharp"><code>ContentRepository.Save(content, SaveAction.Publish, AccessLevel.NoAccess);</code></pre>
<p>This works perfectly but the problem that comes with it that it keeps on calling PublishedContent deligate. Therfore the solution is to use piped SaveAction along with SkipValidation as below - </p>
<pre class="language-csharp"><code>ContentRepository.Save(content, SaveAction.Publish | SaveAction.SkipValidation, AccessLevel.NoAccess);</code></pre>
<p>And if publish method check this enum containing SkipValidation - if true then stop the loop and return it - </p>
<pre class="language-csharp"><code>private static void ContentEvents_PublishedContent(object sender, ContentEventArgs e)
{
if (e is SaveContentEventArgs saveContentEventArgs && saveContentEventArgs.Action.HasFlag(SaveAction.SkipValidation))
{
return;
}
SaveDynamicCategory(e.Content);
}
</code></pre>
<p>Hope it helps. Thank you for your time.</p>Delayed content scheduled job events/blogs/stackmanoz/dates/2022/2/delayed-content-scheduled-job-events/2022-02-28T10:04:38.0000000Z<p>Hello everyone,</p>
<p>Today I was working on content events that adds the products under the category. As soon as you publish the category that will trigger the publish event and associates the products with the category that is being saved. This is working perfectly. </p>
<p>The other scenarios where this was needed as where a category is scheduled to be published at a particular future time then those products should be auto assigned as soon as category is published. Now this required publish events to be triggered. I thought the job that keeps running is "Publish Delayed Content Versions" should handle those events by itself but unfortunately that wasn't supported. </p>
<p>Per many blog posts it was suggested to write your own scheduled job that does it, This is what I'm sharing with you. Hopefully, It saves some time for you in future.</p>
<p>So I ended up writing my own scheduled job that looks for delayed contents and publishes them - </p>
<pre class="language-csharp"><code> private void PublishOnBehalfOf(ContentReference contentLink, string user)
{
IPrincipal currentPrincipal = Thread.CurrentPrincipal;
try
{
Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(user), null);
var content = _contentLoader.Get<ContentData>(contentLink).CreateWritableClone();
if (content != null)
{
_contentRepository.Save((content as IContent), EPiServer.DataAccess.SaveAction.Publish | EPiServer.DataAccess.SaveAction.SkipValidation, AccessLevel.NoAccess);
}
}
finally
{
Thread.CurrentPrincipal = currentPrincipal;
}
}
</code></pre>
<p>Let me know if full code is required. Thanks a lot for reading.</p>Getting Purchase Orders filtered via date from OrderContext/blogs/stackmanoz/dates/2021/3/getting-purchase-orders-from-ordercontext-via-ordersearchparameters/2021-03-17T14:37:09.0000000Z<p>In this blog post I'll be sharing my experience with OrderContext using date b/w condition. </p>
<p><strong>What was the need ?</strong></p>
<p>Get the PurchaseOrders from Commerce based on dates (with between condition) and status. </p>
<p><strong>What was done ?</strong></p>
<p><span>The following pattern is the simplest way to execute order search in Episerver Commerce using the API.</span></p>
<p>Begin the search code with:</p>
<div>
<pre class="language-csharp"><code>OrderSearchOptions searchOptions = new OrderSearchOptions();
searchOptions.StartingRecord = 0; //or whatever you want to specify for paging purposes
searchOptions.RecordsToRetrieve = 10000</code></pre>
<p>To retrieve only Purchase Orders specify <em>'PurchaseOrder'</em> as a search parameter - </p>
<pre class="language-csharp"><code> searchOptions.Classes.Add("PurchaseOrder");</code></pre>
<p>The next thing is to build parameters against the table - </p>
<pre class="language-csharp"><code>// initializes search parameter options to be used while querying against db.
OrderSearchParameters parameters = new OrderSearchParameters();
StringBuilder metaWhere = new StringBuilder();
// specify your conditions, my case it should be the orders with GersOrderNumber column!=null
metaWhere.Append("NOT META.GersOrderNumber IS NULL");
//the dates against the Created Column of OrderGroup_PurchaseOrder table
metaWhere.Append($" AND META.Created>='{startDate}' AND META.Created<='{endDate}'");
parameters.SqlMetaWhereClause = metaWhere.ToString();
// Get the inner query to have OrderGroupId from PurchaseOrder table.
parameters.SqlWhereClause = "OrderGroupId IN (Select ObjectId FROM OrderGroup_PurchaseOrder)";</code></pre>
<p>The last thing is to call the API - </p>
<pre class="language-csharp"><code>PurchaseOrder[] purchaseOrderCollection = OrderContext.Current.Search<PurchaseOrder>(parameters, searchOptions);
</code></pre>
<p>The API would return all Purchase orders within specified conditions.</p>
</div>So finally it happened - Episerver India User Group Meetup/blogs/stackmanoz/dates/2020/2/so-it-finally-happened---episerver-india-user-group-meetup/2020-02-10T10:54:43.0000000Z<p>We proudly hosted Episerver Meetup here in Jaipur today and we were overwhelmed with the responses received.</p>
<p>So we began a little late (you know the hangover of weekend ;)) until we got full-house. </p>
<p><a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=741a7e8c-2586-e711-810b-70106faab5f1">Praful Jangid</a> introduced the Agenda of Meetup as follows - </p>
<ul>
<li>Why we are here?</li>
<li>Presentation I - Content Authoring Experience</li>
<li>Presentation II - How easy is it to begin with Episerver CMS</li>
<li>Product Updates</li>
<li>Next Meetup</li>
<li>Q/A?</li>
</ul>
<p><img src="/link/6a024209f24b432580eb56f83723f9f9.aspx" width="749" height="361" /></p>
<p>Praful talked through the agenda and explained how Episerver impacts customers in India. With his use-case story he impressed everyone there. </p>
<p>Talking of his experience through both CMS & Commerce knowledge, He shared how the customers were fully delighted with the overall experience they had with Episerver's built-in features.</p>
<p><a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=177d84e0-443b-e911-a95e-000d3a3a3a80">Ravindra</a> introduced first session. This was basic fundamentals and self explanatory session. Attendees were most interested and curious at the same time. The questions kept coming and pierced with answers. </p>
<p><img src="/link/b4b5c1d7e5f2454b86a523b1e790195b.aspx" width="763" height="572" /></p>
<p>Ravindra also talked about various knowledge channels where a newbie can kick-start with Episerver CMS development and talked about his experience with Episerver CMS certification process. </p>
<p>And I was the last one to wrap it up, Talking a little bit of Core fundamentals and development. I demonstrated a simple way to start with Episerver CMS development including a Page, Block and personalization. </p>
<p><img src="/link/63b5c851eeb34ba5a2999d6240e57c23.aspx" width="763" height="368" /></p>
<p>With promise to our attendees to organize the next session in next quarter or soon we wrapped it happily and looking forward to the next meetup. Next time, More bigger!</p>
<p>P.S. The sandwich was tasty :D</p>
<p><img src="/link/2261673626ff488eaf4d28502c6d4609.aspx" width="764" height="371" /></p>
<p>Cheers!</p>Using simple address in URL/blogs/stackmanoz/dates/2020/1/using-simple-address-in-url/2020-01-03T14:57:15.0000000Z<p>In this blog post I'll demostrate how you can utilize episerver's Simple address in URL with an Initialization module.</p>
<p>The simple address can be used as a direct address/Route node without language node included in URL.</p>
<p>See Below - </p>
<p><img src="/link/fc5212912a6a475498cd1a4e00f76984.aspx" /></p>
<p>This is an external URL which you can utilize for promotional contents as News Events, Press Releases etc. </p>
<p>Let's create an initialization module that will parse the URL after a virtual path is created.</p>
<p><strong>Step 1 - Define and initialize services to use</strong></p>
<pre class="language-csharp"><code>private bool initialized = false;
private IContentLoader contentLoader;
private IContentRouteEvents contentRouteEvents;
public void Initialize(InitializationEngine context)
{
if (!initialized)
{
contentLoader = context.Locate.Advanced.GetInstance<IContentLoader>();
contentRouteEvents = context.Locate.Advanced.GetInstance<IContentRouteEvents>();
contentRouteEvents.CreatedVirtualPath += ContentRouteEvents_CreatedVirtualPath;
initialized = true;
}
}</code></pre>
<p>Above,</p>
<p>The <strong>contentLoader </strong>would be used to cast the URL to a Page type.</p>
<p>The <strong>contentRouteEvents </strong>would keep tracks of URLs (virtual paths) being created. Note, that the delegate method <strong>ContentRouteEvents_CreatedVirtualPath </strong>will only be raised when outgoing virtual path has been created.</p>
<p>The property <strong>initialized</strong> will initialize the Module only once and it makes sure that it should not be initialized more than once. </p>
<p>This Event handler will be explained in next step. </p>
<p><strong>Step 2 - Managing route via Route Event Handler</strong></p>
<p>In previous step we've defined a delegate method that should be raised when the path is created. Now let's take a look how you can use the URL/Path to cast in a page and utilize it's simple address.</p>
<pre class="language-csharp"><code>private void ContentRouteEvents_CreatedVirtualPath(object sender, UrlBuilderEventArgs e)
{
var contentLink = e.RouteValues[RoutingConstants.NodeKey] as ContentReference;
if (!ContentReference.IsNullOrEmpty(contentLink))
{
var lang = e.RouteValues[RoutingConstants.LanguageKey] as string;
var langSelector = string.IsNullOrEmpty(lang) ? LanguageSelector.AutoDetect() : new LanguageSelector(lang);
var page = contentLoader.Get<IContent>(contentLink, langSelector) as PageData;
if (page != null && !string.IsNullOrEmpty(page.ExternalURL))
{
e.UrlBuilder.Path = page.ExternalURL;
}
}
}</code></pre>
<p>Above, </p>
<p>Read the RouteValues, i.e. language and node name. For instance <a href="http://localhost:52426/en/alloy-track">http://localhost:52426/en/alloy-track</a> <-- language is 'en' and node is 'alloy-track'</p>
<p>Cast the URL as page Data <-- <strong>var page = contentLoader.Get<IContent>(contentLink, langSelector) as PageData</strong></p>
<p>Finally, If the Simple address field is not blank then use the external URL instead of default route URL.</p>
<p><strong>Verifying</strong></p>
<p>When you have the external/Simple Address defined in Episerver then the external URL would be used as the link address of that item. See Below </p>
<p><img src="/link/3231821f23104e1d944f9df59bc39ce9.aspx" /></p>
<p>
<p>Cheers!</p></p>Removing UI Components programmatically/blogs/stackmanoz/dates/2019/10/removing-ui-components-programmatically/2019-10-31T13:51:26.0000000Z<p>In this blog post we are going to have a look on how you can use <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/7.5/D173F03F">ViewTransformer</a> to manipulate your UI. There are situations where we would want to hide particular UI components based on Role. </p>
<p>For an instance, User A with Role <strong><em>CmsApprover</em> </strong>don't need access of Project Items and Media Gadget. In this scenario you can remove those gadgets based on the role given.</p>
<p>The ViewTransformer would let you do it.</p>
<p><strong>Step 1 - Create a class and decorate it with [ViewTransformer] attribute.</strong><strong></strong></p>
<pre class="language-csharp"><code>[ViewTransformer]
public class RemoveComponentsViewTransformer</code></pre>
<p>Using the role - Define a private property with role you want to hide the gadgets against.</p>
<pre class="language-csharp"><code>private string role = "CmsApprover";</code></pre>
<p><strong>Step 2 - Implement IViewTransformer interface.</strong></p>
<pre class="language-csharp"><code>[ViewTransformer]
public class RemoveComponentsViewTransformer : IViewTransformer</code></pre>
<p>The <strong>IViewTransformer</strong> interface has an abstract method called <strong>TransformView</strong> that will let you go through RootComponent and components within it. </p>
<p>I've targetted two components to be hidden for users with Role <strong><em>CmsApprover</em></strong></p>
<pre class="language-csharp"><code>private string[] componentsToRemove = new string[]
{
"EPiServer.Cms.Shell.UI.Components.ProjectModeToolbarComponent",
"EPiServer.Cms.Shell.UI.Components.MediaComponent"
};</code></pre>
<p>You can find these two components under Shell.UI library. Once you browse the reference, You can find those components in the tree - </p>
<p><img src="/link/e40a5937a52948b6803e1b1844a401b5.aspx" /></p>
<p>The <strong>TransformView </strong>method will iterate through the components similarly above tree structure. Therefore, We will iterate through the components and once it reaches to the targetted component then we'll store it in a list and remove it from the RootContainer.</p>
<p><strong>Step 3 - A recursive method that iterates through the component tree until the targetted component is found. </strong></p>
<p> </p>
<pre class="language-csharp"><code>private void BuildListRecursively(IContainer container, List<IComponentMatcher> components)
{
foreach (IComponent component in container.Components)
{
IContainer childContainer = component as IContainer;
if (childContainer != null)
{
BuildListRecursively(childContainer, components);
}
else
{
if (componentsToRemove.Any(item => string.Equals(item,
component.DefinitionName, StringComparison.OrdinalIgnoreCase)))
{
AddComponent(components, component.DefinitionName);
}
}
}
}</code></pre>
<p><strong>Step 4 - Addcomponent and ConfigureComponentMatcher</strong></p>
<p>The above method uses AddComponent method that configures the component and the container. <strong></strong></p>
<pre class="language-csharp"><code>private void AddComponent(List<IComponentMatcher> components, string definitionName)
{
var item = new ConfigurationComponentMatcher(definitionName);
components.Add(item);
}
private class ConfigurationComponentMatcher : IComponentMatcher
{
private readonly string definitionName;
public ConfigurationComponentMatcher(string definitionName)
{
this.definitionName = definitionName;
}
public bool MatchesComponent(IComponent component)
{
return string.Equals(definitionName,
component.DefinitionName, StringComparison.OrdinalIgnoreCase);
}
public bool MatchesContainer(IContainer container)
{
return true;
}
}</code></pre>
<p>The <strong>ConfigurationComponentMatcher </strong>method determines the Component's definition name matches with the targetted component's name which is current iteration item. It returns the match.</p>
<p><strong>Step 5 - TransformView </strong></p>
<p>Finally the abstract method looks like this -</p>
<pre class="language-csharp"><code>public void TransformView(ICompositeView view, IPrincipal principal)
{
// build a list of components to remove
var components = new List<IComponentMatcher>();
BuildListRecursively(view.RootContainer, components);
// remove the components for the specified user
var hasCMSApproverRole = principal.IsInRole(role);
if (hasCMSApproverRole)
{
view.RootContainer.RemoveComponentsRecursive(components,
notifyComponentOnRemoval: false);
}
}</code></pre>
<p><strong>Verifying</strong></p>
<p>When you run your application, You can see the users with Role <strong>CmsApprover</strong> have two gadgets <strong>ProjectToolbar</strong> and <strong>MediaToolbar</strong> hidden for them. I've highlighted them below where they should have been appeared with non CmsApprover roles.</p>
<p><img src="/link/69f8213e85f64fa6b7e32e734f08f79b.aspx" width="884" height="542" /></p>
<p>Have a great day!</p>Adding a requirejs block to your site/blogs/stackmanoz/dates/2019/9/adding-a-require-js-block-to-your-site/2019-09-26T16:57:57.0000000Z<p>Do you love require.js? who don't!</p>
<p>There are some reasons:</p>
<ul>
<li> In a large application a lot of JavaScript files are needed, and each script tag needs a request.</li>
<li> You have to put them in a same order in which they are called, i.e. File which is dependent on other should be loaded after the dependent ones.</li>
</ul>
<p>Well that's an overview but I'm not going to talk about it. This blog post is about adding a require block from episerver itself.</p>
<p><strong>What we'll be creating?</strong></p>
<p>A block, A partial view and couple of requireJS configurations and nothing else..</p>
<p>Before beginnging to first step, Please make sure you've downloaded javascript library of require.js. You can find it <a href="https://requirejs.org/docs/download.html">here</a></p>
<p>In the end of this blog post our result would look like this -</p>
<p><img src="/link/6336be19a01345a69247d7d0ea288cb4.aspx" /></p>
<h2><strong>Step 1 - Let's start with the configuration of require.js </strong></h2>
<p>Begin with configuring your javascript libraries by defining their paths with <a href="https://requirejs.org/docs/jquery.html#modulename">require.config</a> method. I call this file <strong>main.js</strong> and it looks something like this. Ofcourse you can remove unwanted scripts. </p>
<pre class="language-javascript"><code>require.config({
baseUrl: "/Static",
paths: {
'jquery': '/Static/js/jquery',
'jquery_validate': '/Static/js/jquery.validate',
'jquery_validate_unobtrusive': '/Static/js/jquery.validate.unobtrusive',
'bootstrap': '/Static/js/bootstrap',
'utils': '/Areas/Country/Static/js/App/utils',
'imagesloaded': '/Static/js/imagesloaded.pkgd.min',
'jQueryBridget': '/Static/js/jquery-bridget',
'masonry': '/Static/js/masonry.pkgd.min',
'jquery_mask': '/Static/js/jquery.maskedinput',
'bootstrap-dialog': '/Static/js/bootstrap-dialog',
'bxslider':'/Areas/Country/Static/js/App/jquery.bxslider.min'
},
waitSeconds: 0,
shim: {
"jquery.validate": {
'deps': ['jquery'],
'exports': 'validate'
},
"jquery.validate.unobtrusive": {
'deps': ['jquery', 'jquery_validate'],
'exports': 'unobtrusive'
},
"bootstrap": {
'deps': ['jquery']
},
"utils": {
'deps': ['jquery']
},
"jQueryBridget": {
'deps': ['jquery']
},
"imagesloaded": {
'deps': ['jquery']
},
"masonry": {
'deps': ['jquery', 'jQueryBridget', 'imagesloaded']
},
"jquery_mask": {
'deps': ['jquery'],
'exports': 'jQuery.fn.mask'
},
"bootstrap-dialog": {
'deps': ['jquery', 'bootstrap']
},
"bxslider": {
'deps': ['jquery']
}
}
});</code></pre>
<h2>Step 2 - Creating a block </h2>
<pre class="language-csharp"><code>[ContentType(DisplayName = "Require Block", GroupName = "Framework", GUID = "986db6dd-9c9d-4b9c-bd83-cff05c0f7f93", Description = "Require wrapper function definition that expose jquery, bootstrap and other libraries.($).")]
public class RequireBlock : BlockData
{
[UIHint(UIHint.Textarea)]
[CultureSpecific]
[Display(
Name = "Script Content",
Description = "The script block content.",
GroupName = SystemTabNames.Content,
Order = 10)]
public virtual string ScriptContent { get; set; }
[CultureSpecific]
[Display(
Name = "Use Jquery",
Description = "Include jquery.js in the require definition.",
GroupName = SystemTabNames.Content,
Order = 20)]
public virtual bool UseJquery { get; set; }
[CultureSpecific]
[Display(
Name = "Use Bootstrap",
Description = "Include boostrap.js in the require definition.",
GroupName = SystemTabNames.Content,
Order = 30)]
public virtual bool UseBootstrap { get; set; }
[CultureSpecific]
[Display(
Name = "Use Utils",
Description = "Include utils.js in the require definition.",
GroupName = SystemTabNames.Content,
Order = 40)]
public virtual bool UseUtils { get; set; }
[CultureSpecific]
[Display(
Name = "Custom Libraries",
Description = "Include custom libraries in the require definition",
GroupName = SystemTabNames.Content,
Order = 50)]
[EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<RequireBlockCustomJavascriptLibrary>))]
public virtual IList<RequireBlockCustomJavascriptLibrary> CustomLibaries { get; set; }
}
public class RequireBlockCustomJavascriptLibrary
{
public string Libary { get; set; }
public string ExportSymbol { get; set; }
}
[PropertyDefinitionTypePlugIn]
public class RequireBlockCustomJavascriptLibraryProperty : PropertyListBase<RequireBlockCustomJavascriptLibrary>
{
}</code></pre>
<p>We are doing a couple of steps here</p>
<ul>
<li>A multiline textarea that would facilitate us to write a script within</li>
<li>Property definition to use the custom class <strong>RequireBlockCustomJavascriptLibrary</strong></li>
</ul>
<p>Read more about<a href="/link/c91dd4b0574942258ceecc719e0d0b56.aspx"><strong> PropertyDefinitionTypePlugIn</strong></a></p>
<p><strong>Step 3 - Create a view</strong></p>
<pre class="language-csharp"><code>@model RequireProject.Framework.Web.Models.Blocks.RequireBlock
@{
List<KeyValuePair<string, string>> libraries = new List<KeyValuePair<string, string>>();
if (Model.UseJquery == true)
{
libraries.Add(new KeyValuePair<string, string>("'jquery'", "$"));
}
if (Model.UseBootstrap == true)
{
libraries.Add(new KeyValuePair<string, string>("'bootstrap'", "Bootstrap"));
}
if (Model.UseUtils == true)
{
libraries.Add(new KeyValuePair<string, string>("'utils'", "Utils"));
}
if (Model.CustomLibaries != null && Model.CustomLibaries.Any())
{
foreach(var item in Model.CustomLibaries)
{
libraries.Add(new KeyValuePair<string, string>(string.Format("'{0}'", item.Libary), item.ExportSymbol));
}
}
List<string> allKeys = (from kvp in libraries select kvp.Key).Distinct().ToList();
string keys = string.Join(",", allKeys);
List<string> allSymbols = new List<string>();
foreach (string key in allKeys)
{
allSymbols.Add(libraries.FirstOrDefault(o => o.Key == key).Value);
}
string symbols = string.Join(",", allSymbols);
}
<script>
require(['/main.js'], function(){
require([@Html.Raw(keys)]
, function (@Html.Raw(symbols)) {
@Html.Raw(Model.ScriptContent)
});
});
</script>
</code></pre>
<p>So above here,</p>
<ul>
<li>We'll initially put the configuration file <strong>main.js </strong>that already has the mapping of paths where our javascript libraries are in the filesystem.</li>
<li>Combining exported script references along with the scripts which a user selects via checkbox.</li>
<li>We'll have an export symbol along with the references given within require block script array, You will see an example below</li>
</ul>
<p>And that's done.</p>
<h2><strong>Usage</strong></h2>
<p>Now create a require block in episerver and do the followings, shown in below image -</p>
<p>Note that Custom Libraries would open a popup with 2 properties, first with path and the other one is the export symbol.</p>
<p><strong><img src="/link/01da80c4d204451a952a244076341ade.aspx" /></strong></p>
<h2><strong>Output</strong></h2>
<p>Doing all above would render a script on your page (wherever you use the block in epi), shown example below -</p>
<pre class="language-javascript"><code><script>
require(['/main.js'], function(){
require(['jquery','https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.js']
, function ($,masonry) {
console.log('testing');
});
});
</script></code></pre>
<p>Note above, Since we enabled jQuery checkbox from the page that's why it has now included the 'jquery' and the export symbol (symbol written on view).</p>
<p>Hope you liked it. I'm opened to your suggestion.</p>
<p>Have a good day!</p>Accessing ContentReference in schedule job or Initialization module where you receive HttpContext as null/blogs/stackmanoz/dates/2019/8/accessing-contentreference-in-schedule-job-or-wherever-you-receive-httpcontext-as-null/2019-08-26T12:56:24.0000000Z<p>Often you want to work with content in an initialization module or a scheduled job and use a reference to the Start page. But in these scenarios, there is no HTTP context, so the system does not know what site you want to work with. </p>
<p>One fix is to set a wildcard * as a hostname in the configuration of one of you sites. This site will then be returned by ContentReference.StartPage.</p>
<p>Here is an example of how you can customize this. Go to your website > Admin> config > Manage Websites and do the following - </p>
<p><img src="/link/b129d9e8e45f421dbcf75d1cc9a69ff3.aspx" width="491" height="226" /></p>
<p>It should now give you a reference of StartPage of the site, see below- </p>
<p><img src="/link/f0abee1174c6455ab2cdd6caceba922f.aspx" width="491" height="76" /></p>
<h2><strong>Better Alternative</strong></h2>
<p>A more flexible alternative is to use the <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/10/8F4A2DC8">ISiteDefinitionRepository</a> to get a list of all sites and write logic to select the one you want to work with.</p>
<p>Here is how you can make usage of this repository - </p>
<pre class="language-csharp"><code>var siteDefinitionRepo = ServiceLocator.Current.GetInstance<ISiteDefinitionRepository>();
var siteDefinitions = siteDefinitionRepo.List().ToList();</code></pre>
<p>Now you can work with the site item which we've just got via this Repository.</p>
<p>On a side note, However I've created an instance of above repository just for a demo purpose but you should always inject your dependencies and get them resolved with <span>IOC container like <a href="/link/ebeb7390a6de4c16a2530221b226e292.aspx">StructureMap</a> which Episerver recommends.</span></p>
<p><span>Have a great day!</span></p>Making use of TypeConventionBuilder to include a field with Find/blogs/stackmanoz/dates/2019/7/making-use-of-typeconventionbuilder-to-include-a-field-with-find/2019-07-26T11:20:48.0000000Z<p>The prerequisite for this post is a basic knowledge of Episerver.Find.</p>
<p>Yesterday I came accross with an issue with <a href="/link/5a210841b3a34512b48fa3ad5f56c667.aspx">Episerver.Find filters</a>. If you've ever faced issues with XhtmlString values with Episerver.Find then this blog post may help you.</p>
<p>I had an <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/7/2ddb34c2-d602-6b06-a596-8cb8af3b41fa">XhtmlString</a> as one of the properties within the block as follows - </p>
<pre class="language-csharp"><code>[CultureSpecific]
[Display(
Name = "Title",
Description = "Title to display.",
GroupName = SystemTabNames.Content,
Order = 400
)]
public virtual XhtmlString Title { get; set; }</code></pre>
<p>And I wanted to sort the blocks based on <code>Title</code> (typeof(XhtmlString)) with <a href="https://find.episerver.com/">Find</a> query - </p>
<pre class="language-csharp"><code>SearchClient.Instance.Search<MyCustomBlock>()
.For(searchTerm)
.OrderBy(c => c.Title)
.GetContentResult<MyCustomBlock>();</code></pre>
<p>I experienced this wouldn't sort the blocks based on OrderBy Title. Because the string type here would return Xhtml elements and ultimately this would not sort it properly. </p>
<p>In case if I had the <code>Title</code> as plain string property in block then it should have sorted it as expected. But I didn't like the idea to add any additional field of type string instead of XhtmlString to existing block.</p>
<h4>So what should have been done?</h4>
<p>Keeping this question in mind, I posted a forum post and as expected this finally put the results in. I can't thank enough to <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userid=c2dbd5e1-f46d-dd11-a1d1-0018717a8c82">Paul</a> to sort this out for me. </p>
<h4>Here is how it worked</h4>
<p>If you could get rid of those HTML elements anyhow within the XhtmlString then it would definitely be a way to go - </p>
<h5>Step 1</h5>
<p>Creating an extension method to strip out HTML from XhtmlString - </p>
<pre class="language-csharp"><code> public static class SearchExtensions
{
public static string StripHtml(this XhtmlString htmlField)
{
return htmlField == null ? string.Empty : EPiServer.Core.Html.TextIndexer.StripHtml(htmlField.ToHtmlString(), 1000);
}
}</code></pre>
<p>This is using an existing method <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/10/51C20DD7">TextIndexer</a> class to strip out those HTML elements. Which <span>returns up to 1000 characters (which should be easily sufficient for sorting).</span></p>
<h5>Step 2</h5>
<p>Create an <a href="/link/066f9a07d124494fa69edd5ec4f87b07.aspx">InitializableModule </a>that would use <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=find/8/B59914AE">TypeConventionBuilder</a> to include a field in Episerver.Find Indexed contents and use the extension method created above - </p>
<pre class="language-csharp"><code>[InitializableModule]
[ModuleDependency((typeof(EPiServer.Web.InitializationModule)))]
public class InitialiseSearch : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
SearchClient.Instance.Conventions.ForInstancesOf<MyCustomBlock>().IncludeField(x => x.Title.StripHtml());
}
public void Uninitialize(InitializationEngine context)
{
}
}</code></pre>
<p>This will include field to find index with the key - <strong>Title.StripHtml$$string </strong>and this is the key that would do our job done.</p>
<h5>Step 3</h5>
<p>Modifying Find query to use this extension method - </p>
<pre class="language-csharp"><code>....
.OrderBy(c => c.Title.StripHtml())</code></pre>
<h5>Step 4</h5>
<p>Now you would need to reindex your contents with the scheduled job from Episerver's Admin area - <strong>EPiServer Find Content Indexing Job</strong></p>
<p><span>After you've reindexed, It modifies the way the content is indexed, adding in a field to the content items in the index called "</span><span>Title</span><span>.StripHtml$$string". When you search or order on </span><span>Title.StripHtml(), it doesn't run the extension method, instead it looks for the "Title.StripHtml$$string" field. If you haven't reindexed, that field won't be there.</span></p>
<p><span>Verifying the index - </span></p>
<p><span>Go to Episerver Find/overview and click on Explore and then search for your block. This should have the field added to the Find index and look closely it is a plain string now.</span></p>
<p><img src="/link/ae2dfb9af30449ccbc1e3b864b4de2d9.aspx" /></p>
<p>Have a great day!</p>
<p><code></code></p>Using ExtendedMetadata from SelectionFactory /blogs/stackmanoz/dates/2019/6/reading-metadata-from-selectionfactory-/2019-06-26T12:26:49.0000000Z<p>Prerequisite - </p>
<p>This tutorial requires a knowledge of <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/11/DFE1C493">ISelectionFactory.</a></p>
<p>In this tutorial I would like to share my experience with SelectionFactory features from Episerver.</p>
<p>We had a requirement where an Episerver CMS user should be able to view the options based on certain parameters. Consider the case where you have a multiple options type fields within the CMS those should be fetching their values according to input parameters via attribute. </p>
<h3>Step 1 - Creating an abstract SelectionFactory class</h3>
<p>We are going to inherit the Interface <strong>ISelectionFactory</strong> from <strong>EPiServer.Shell.ObjectEditing </strong>Assembly and creating our custom <strong>LookupSelectionFactory </strong>as a service factory.<strong></strong></p>
<p>I would have an additional method <strong>AddItems</strong> of type virtual to be able to override in derived class (the class which inherits LookupSelectionFactory).</p>
<p>Note that It has two parameteres and one of them is <a href="/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=episerverframework/7/41fd2570-c368-cec3-1ca2-5df6003e9530"><strong>ExtendedMetadata</strong> </a>that we are going to use to fetch the values from attribute.</p>
<pre class="language-csharp"><code>public abstract class LookupSelectionFactory : ISelectionFactory
{
protected virtual IEnumerable<ISelectItem> AddItems(RestClient client, ExtendedMetadata metadata)
{
throw new NotImplementedException();
}
}</code></pre>
<h3>Step 2 - Creating an Attribute class (to input desired parameters)</h3>
<pre class="language-csharp"><code>[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
public class PickListAttribute : System.Attribute
{
public string AttributeName { get; set; }
public string AttributeType { get; set; }
}</code></pre>
<p><strong>AttributeTargets.All</strong> - Specifies that <em>Attribute can be applied to any application element. Whether it's a constructor, class, delegate, event etc.</em></p>
<p><strong>AllowMultiple</strong> - <em>Gets or sets a Boolean value indicating whether more than once instance of the indicated attribute can be specified for a single program element.</em></p>
<p><strong>Inhertied</strong> - <em>Gets or sets a Boolean value that determines whether the indicated attribute is inherited by derived class and overriding members.</em></p>
<p><em></em></p>
<h3>Step 3 - Using factory on Block property</h3>
<pre class="language-csharp"><code>[CultureSpecific]
[Display(
Name = "Title Options", Description = "Title Options", GroupName = FormTabGroups.ABOUT_YOU_TAB, Order = 1)]
[PickListAttribute(AttributeType = "CONTACT", AttributeName = "TITLE")]
[SelectMany(SelectionFactoryType = typeof(ShowOptionsSelectionFactory))]
public virtual string TitleOptions { get; set; }</code></pre>
<p>In the next step > create <strong>ShowOptionsSelectionFactory </strong>that will display the Options in Episerver.</p>
<h3>Step 4 - Inheriting abstract factory and reusing it in custom selection factory for our purpose</h3>
<pre class="language-csharp"><code> public class ShowOptionsSelectionFactory : LookupSelectionFactory
{
protected override IEnumerable<ISelectItem> AddItems(RestClient client, ExtendedMetadata metadata)
{
List<ISelectItem> items = new List<ISelectItem>();
try
{
PickListAttribute pickListAttribute = (PickListAttribute)metadata.Attributes.FirstOrDefault(c => c is PickListAttribute);
string name = pickListAttribute.AttributeName;
string type = pickListAttribute.AttributeType;
items.Add(new SelectItem()
{
Value = type,
Text = name,
});
}
catch (Exception ex)
{
//catch the exception here
}
return items;
}
}</code></pre>
<p>The line above picks the attribute of type <strong>PickListAttribute </strong>amongst other Attributes on the same property <strong>- [CultureSpecific], [Display], [SelectMany] </strong>with the help of <strong>metadata</strong> parameter.</p>
<p><code></code></p>
<pre class="language-csharp"><code>(PickListAttribute)metadata.Attributes.FirstOrDefault(c => c is PickListAttribute) </code></pre>
<p><code></code></p>
<p>The metadata keeps the Attributes information along with it that you can pull based on your needs.</p>
<p>For instance if you want to fetch the values for Display attribute being used on the property <strong>TitleOptions</strong>, Then you can query within <strong>Attributes</strong> property from <strong>metadata</strong> and cast it like this - </p>
<pre class="language-csharp"><code>DisplayAttribute crmPickListAttribute = (DisplayAttribute)metadata.Attributes.FirstOrDefault(c => c is DisplayAttribute);</code></pre>
<p>Enjoy your day!</p>