Blog posts by Joshua Folkerts2019-10-30T21:38:39.0000000Z/blogs/Joshua-Folkerts/Optimizely World2 Factor Authentication In EPiServer/blogs/Joshua-Folkerts/Dates/2019/2/2-factor-authentication-in-episerver/2019-10-30T21:38:39.0000000Z<p>A client approached us one day asking if we could setup 2 factor authentication on their EPiServer site based on some requires their Information Technology team requested due to security concerns. I am not sure what concerns they had but we thought it would be a good challenge and a fun little project. We know there has been a lot of posts going around discussing how secure is 2FA and how beatable is it. We are not going to dive into all the ways people are trying to beat the system but more how to enable 2FA in your Episerver site. </p>
<p>There are some items you need to consider here first. If you have a large user database already using asp.net memberships and roles, you will need to convert those over to Identity users. We ended up writing a scheduled job for converting the users over to identity users and sending them a temporary password to handle the transition a bit smoother. The users will also need to have an application available for generating the token. In our case, any OTP application will work as we use OTP for generating the One Time Password.</p>
<p>There are quite a few pieces that we need to hook into here to get EPI to play nicely will 2FA. They are not hard I promise that but will take some time and tinkering to make tie it altogether.</p>
<p>The first thing we need to do is make sure the tables are created in your application for Identity to use for our user store. If you do not have a table, I have included them in the link below in github under the TwoFactorAuth folder. Once those tables are created, optional if you have them already, is to handle the packages needed for 2FA. We are going to install the OTPSharp.Core package. Note, this assumes you already have OWIN installed already. If you do not, you will need to install the following packages:</p>
<pre class="language-markup"><code>Install-Package Microsoft.AspNet.Identity.Owin
Install-Package Microsoft.Owin.Security.OAuth
Install-Package Microsoft.Owin.Security.Cookies
Install-Package OtpSharp.Core
Install-Package Wiry.Base32.Patched
</code></pre>
<p>Now that we have those packages installed, we can now wire up epi to let OWIN know a couple things at startup needed to run 2FA. We need to tell the system to use our provider as well as we need to tell the system to use our new ApplicationUser model which inherits EPiServer.Cms.UI.AspNetIdentity.ApplicationUser. By doing this we get the benefit of adding our 2 custom fields to the Identity object which will store the <strong>IsTwoFactorAuthenticatorEnabled</strong> boolean for letting the system know we have another step before we are authenticated, and a field for the <strong>TwoFactorAuthenticatorSecretKey</strong> to help with the token. </p>
<p>So in startup, we are going to add our cod to wire up the 2FA to the system:</p>
<pre class="language-csharp"><code>public void Configuration(IAppBuilder app)
{
// Register our two factor auth into the site.
app.AddCmsTwoFactorAspNetIdentity<SiteApplicationUser>();
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Add CMS integration for ASP.NET Identity
// app.AddCmsAspNetIdentity<ApplicationUser>();
// Remove to block registration of administrators
app.UseAdministratorRegistrationPage(() => HttpContext.Current.Request.IsLocal);
// Use cookie authentication
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(Global.LoginPath),
Provider = new CookieAuthenticationProvider
{
// If the "/util/login.aspx" has been used for login otherwise you don't need it you can remove OnApplyRedirect.
OnApplyRedirect = cookieApplyRedirectContext =>
{
app.CmsOnCookieApplyRedirect(cookieApplyRedirectContext, cookieApplyRedirectContext.OwinContext.Get<ApplicationSignInManager<ApplicationUser>>());
},
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager<ApplicationUser>, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => manager.GenerateUserIdentityAsync(user))
}
});
}
</code></pre>
<p>I do want to point out the bold section of the Configuration Method. We need to intercept EPiServers “AddCmsAspNetIdentity” method to configure our service which allows us to tell episerver to use our SiteApplicationUser and to configure our password policy if we want too. We also configure the new 2FA Provider in the AddCmsTwoFactorAspNetIdentity Method. See how this is done below</p>
<pre class="language-csharp"><code>public static IAppBuilder AddCmsTwoFactorAspNetIdentity<TUser>(this IAppBuilder app) where TUser : IdentityUser, IUIUser, new()
{
var applicationOptions = new ApplicationOptions
{
DataProtectionProvider = app.GetDataProtectionProvider()
};
// Configure the db context, user manager and signin manager to use a single instance per request by using
// the default create delegates
app.CreatePerOwinContext(() => applicationOptions); app.CreatePerOwinContext<ApplicationDbContext<TUser>>(ApplicationDbContext<TUser>.Create); app.CreatePerOwinContext<ApplicationRoleManager<TUser>>(ApplicationRoleManager<TUser>.Create); app.CreatePerOwinContext<ApplicationUserManager<TUser>>(CreateApplicationUserManager);
// 2 Factor Auto Registration app.CreatePerOwinContext<ApplicationSignInManager<TUser>>(ApplicationSignInManager<TUser>.Create);
// Configure the application
app.CreatePerOwinContext<UIUserProvider>(ApplicationUserProvider<TUser>.Create); app.CreatePerOwinContext<UIRoleProvider>(ApplicationRoleProvider<TUser>.Create); app.CreatePerOwinContext<UIUserManager>(ApplicationUIUserManager<TUser>.Create); app.CreatePerOwinContext<UISignInManager>(ApplicationUISignInManager<TUser>.Create);
// Saving the connection string in the case dbcontext be requested from none web context
ConnectionStringNameResolver.ConnectionStringNameFromOptions = applicationOptions.ConnectionStringName;
return app;
}
public static ApplicationUserManager<TUser> CreateApplicationUserManager<TUser>(IdentityFactoryOptions<ApplicationUserManager<TUser>> options, IOwinContext context) where TUser : IdentityUser, IUIUser, new()
{
var manager = new ApplicationUserManager<TUser>(new UserStore<TUser>(context.Get<ApplicationDbContext<TUser>>()))
{
UserLockoutEnabledByDefault = true,
DefaultAccountLockoutTimeSpan = TimeSpan.FromMilliseconds(5),
MaxFailedAccessAttemptsBeforeLockout = 5,
PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = false,
RequireLowercase = false,
RequireUppercase = false
}
};
manager.UserValidator = new UserValidator<TUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
manager.RegisterTwoFactorProvider("TwoFactor Authenticator", (IUserTokenProvider<TUser, string>)new TwoFactorAuthenticatorTokenProvider());
var provider = context.Get<ApplicationOptions>().DataProtectionProvider.Create("EPiServerAspNetIdentity");
manager.UserTokenProvider = new DataProtectorTokenProvider<TUser>(provider);
return manager;
}
</code></pre>
<p>A lot of this code should look familiar as it is in the default Alloy website. The three items we are more concerned about here is the top three lines of our startup.cs file. The first line is to tell the system to use our SiteApplicationUser instead of EPiServer default ApplicationUser store. The second line is to use 2FA in the login process. The third is to allow browser cookie remembering. The other thing to note here is we must replace all the <strong>ApplicationUser</strong> objects with our new <strong>SiteApplicationUser</strong> (or whatever you named your model) but I will be referring to my naming in this sample. </p>
<p>Now we have most of the inner plumbing done to wire it up, minus the controllers and views to help us along the way, if we to build the application, it should build for you, if not, there might be some namespaces or packages that need to be added yet but it should build for you. From here, we are going to add another setup in the registrationcontroller that EPiServer kindly provided in the alloy demo site. What we are trying to achieve here is to let the application know that we need to redirect the user to the 2fa screen to setup a new token in their OTP app. Based on my testing and setup of allow, I have use Microsoft Authenticator, Google Authenticator, TOTP Authenticator, and 1Password which all worked in the code I will provide at the end of the post. Since we are not deriving our registration controller for an EPiServer page, we are going to need a route to tell the system our new controller and action are available. This is just the standard default mvc route to a default action and controller, but you can narrow it down to just the account controller if need be.</p>
<p>In the Global.asax.cs file, I am going to insert a default controller last in the pipeline to ensure that we don’t mess with any of EPiServer’s routes. </p>
<pre class="language-csharp"><code>protected override void RegisterRoutes(RouteCollection routes)
{
base.RegisterRoutes(routes);
RouteTable.Routes.MapRoute(
name: "DefaultRoutes",
url: "{controller}/{action}",
defaults: new { action = "Index" });
}
</code></pre>
<p>Here I am just telling the system to accept any controller and action as a default route along with adding it to the routing table’s routes. This is by default normally wired up for you in a default mvc application out of the box. </p>
<p>Now that we have the route available, we are going to add the accountcontroller that handles enabling the 2FA for users once they have logged in. So the process is as follows. A user logins/registers for the first time, upon registration, if they have a 2fa token, they will be directed to a second screen to allow them to enter their OTP. If the user has not setup an OTP yet, they will be directed to a different screen for them to scan a QR code or copy and past the image for them to generate the proper secret key for them to enter. Once this all happens, the user is finally authenticated and can begin using the system. This is all there is for the registration/login process of 2fa. Below is a snippet from the enabletwofactorauth action that generates the QR code for the user and the post method which reads the secret key and validates it against the TOTP. </p>
<pre class="language-csharp"><code>[HttpGet]
public ActionResult EnableTwoFactorAuth()
{
byte[] secretKey = KeyGeneration.GenerateRandomKey(20);
string userName = User.Identity.GetUserName();
string issuer = SiteDefinition.Current.Name;
string issuerEncoded = HttpUtility.UrlEncode(issuer);
string barcodeUrl = KeyUrl.GetTotpUrl(secretKey, userName) + "&issuer=" + issuerEncoded;
var model = new TwoFactorAuthenticatorViewModel
{
SecretKey = Base32Encoding.Standard.GetString(secretKey),
BarcodeUrl = barcodeUrl
};
return View("~/TwoFactorAuth/Account/EnableTwoFactorAuth.cshtml", model);
}
[HttpPost]
public async Task<ActionResult> EnableTwoFactorAuth(TwoFactorAuthenticatorViewModel model)
{
if (ModelState.IsValid)
{
byte[] secretKey = Base32Encoding.Standard.ToBytes(model.SecretKey);
var otp = new Totp(secretKey);
if (otp.VerifyTotp(model.Code.Trim(), out long timeStepMatched, new VerificationWindow(2, 2)))
{
var user = UserManager.FindById(User.Identity.GetUserId());
user.IsTwoFactorAuthenticatorEnabled = true;
user.TwoFactorAuthenticatorSecretKey = model.SecretKey;
user.TwoFactorEnabled = true;
await UserManager.UpdateAsync(user);
return RedirectToLocal(UrlResolver.Current.GetUrl(SiteDefinition.Current.StartPage));
}
else
{
ModelState.AddModelError("Code", "The Code is not valid");
}
}
return View("~/TwoFactorAuth/Account/EnableTwoFactorAuth.cshtml", model);
}
</code></pre>
<p>This really is what starts the whole 2fa sequence moving. Once they have a code, they are redirected to the start page where they should be able to get to edit or admin mode straight away. </p>
<p>For users that already have 2fa enabled on their identity account, they will be taken to the verify code page that will ask the user to enter their 2fa 6 digit code. Below it should looks something like this.</p>
<pre class="language-csharp"><code>[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl, bool rememberMe = false)
{
// Require that the user has already logged in via username/password or external login
if (!await SignInManager.HasBeenVerifiedAsync())
{
return View("Error");
}
if (string.IsNullOrWhiteSpace(provider))
provider = "TwoFactor Authenticator"; // Name of your provider registered in ApplicationBuilderExtensions
return View("~/TwoFactorAuth/Account/VerifyCode.cshtml", new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View("~/TwoFactorAuth/Account/VerifyCode.cshtml", model);
}
// The following code protects for brute force attacks against the two factor codes.
// If a user enters incorrect codes for a specified amount of time then the user account
// will be locked out for a specified amount of time.
// You can configure the account lockout settings in IdentityConfig
var result = await SignInManager.TwoFactorSignInAsync(
model.Provider,
model.Code,
isPersistent: model.RememberMe,
rememberBrowser: model.RememberBrowser);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(model.ReturnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid code.");
return View("~/TwoFactorAuth/Account/VerifyCode.cshtml", model);
}
}
</code></pre>
<p>Here they are redirected to the start page or the return url that we are passing through just as asp.net membership provider does. Having described all this, this is just a simple working copy in the Alloy sample website. You can change it to fit your business requirement needs but this is more for getting you started with 2 Factor Authentication.</p>
<p>Before you begin, one thing to note is the registration has been modified and once you register a new user with Alloy, you will need to logout and log back in. The login url should resolve to “/account/login”. This will now force you to enter your verification information. I have added a url redirect to handle the util/login.aspx for some reason, I can not tie into the submission event. If you all know of a way, I am all ears.</p>
<p>After you login, you will be presented with the following screen. This action resolves to “/account/enabletwofactorauth”. This is where you scan the code and set the the 6 digit code to create the secret.</p>
<p><img src="/link/b875144b77254faf8e8ea96e29db710c.aspx" /></p>
<p>Once you enable by clicking the button, you will be logged in. If you logout and log back in, you are presented with the following screen which will ask you for your 6 digit code.</p>
<p><img src="/link/8e60aa7832de47ed9d378d544d741421.aspx" /></p>EPiServer Notification For Expiring Pages/blogs/Joshua-Folkerts/Dates/2019/2/episerver-notification-for-expiring-pages/2019-02-16T19:16:11.0000000Z<p>A client asked if they could recieve some form of notification that pages they have created are about to expire 7 days before it actually expired. I asked the client if they wanted an email of the list of the pages that are about to be expired and their response was, we get so many emails a day, that it might get lost in the list. So the next best thing is for me to use the notification service that will send a notification to the user in the cms in edit mode. I know episerver uses the the notification service to tell us when their is a new version of tinymce, etc, but i wanted to use it to inform the user that these x number of pages are about to expire. </p>
<p>With the thought of using the notication servier, I went perusing around the documentation to find some information on how to tie into the notification service. After reading the documentation, I came up with a scheduled job that will grab all the pages in the site tree and check the stop publish date to see if it is about to expire. When the page is about to expire, i add it to the notification queue to notifiy the editor that this page is about to expire.</p>
<p>There are many ways and services that allow you to do this exact thing but i wanted to keep it simple and allow the admin to control how often to check the expiriy of the the page. My goal is to keep is self contained regardless what cloud service or on premise hosting enviroment you chose. </p>
<p>So to give an overview on what I did to accomplish this, </p>
<ol>
<li>Create a scheduled job that will parse the pages for expiring pages</li>
<li>Implement the IUserNotificationRepository to create new notifications to send</li>
<li>Send the notification to the user based on pages expirey</li>
</ol>
<p>This is the scheduled job i used to send the expiration of the pages to the user.</p>
<pre class="language-csharp"><code>using Blend.Episerver;
using EPiServer.Core;
using EPiServer.Editor;
using EPiServer.Notification;
using EPiServer.PlugIn;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Routing;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Linq;
namespace EPiServerSamples.ScheduledJobs
{
[ScheduledPlugIn(DisplayName = "Expiring Pages Notification")]
public class ExpiringPagesNotification : Blend.Episerver.ScheduledJobs.BlendJobBase
{
bool _stopSignaled;
readonly INotifier notifier;
readonly IUserNotificationRepository userNotificationService;
const string ExpiryChannelName = "epi.PageExpiry";
public ExpiringPagesNotification()
{
IsStoppable = true;
var locator = ServiceLocator.Current;
notifier = locator.GetInstance<INotifier>();
this.userNotificationService = locator.GetInstance<IUserNotificationRepository>();
}
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
_stopSignaled = true;
}
/// <summary>
/// Called when a scheduled job executes
/// </summary>
/// <returns>A status message to be stored in the database log and visible from admin mode</returns>
public override string Execute()
{
//Call OnStatusChanged to periodically notify progress of job for manually started jobs
OnStatusChanged(string.Format("Starting execution of {0}", this.GetType()));
var pages = this.GetExpiringPages();
this.SetCounter("Expiring Pages", pages.Count());
foreach (var page in pages)
{
var category = new Uri(UrlResolver.Current.GetUrl(page.ContentLink));
var notifications = AsyncContext.Run(() => this.userNotificationService.GetUserNotificationsAsync(new UserNotificationsQuery()
{
ChannelName = ExpiryChannelName,
Category = category
}, 0, 20));
if (!notifications.PagedResult.HasValue())
{
AsyncContext.Run(() => notifier.PostNotificationAsync(new NotificationMessage()
{
ChannelName = ExpiryChannelName,
Content = $"{page.Name}(<a href=\"{ PageEditing.GetEditUrl(page.ContentLink)}\">{page.ContentLink}</a>) is about to Expire on {page.StopPublish.Value.ToShortDateString()} - {page.StopPublish.Value.ToShortTimeString()}",
Subject = "Expiration of Page Upcoming",
Recipients = new[] { new NotificationUser(page.CreatedBy) },
Sender = new NotificationUser("System"),
TypeName = "PageExpiring",
Category = new Uri(UrlResolver.Current.GetUrl(page.ContentLink))
}));
this.Increment("Messages Sent");
}
//For long running jobs periodically check if stop is signaled and if so stop execution
if (_stopSignaled)
{
return "Stop of job was called";
}
}
return this.CounterReport();
}
private IEnumerable<PageData> GetExpiringPages() =>
this.contentLoader.GetItems(this.contentLoader.GetDescendents(SiteDefinition.Current.StartPage), new LoaderOptions())
.Where(x => x is IVersionable && ((IVersionable)x).StopPublish.GetValueOrDefault(DateTime.MaxValue) < DateTime.Now.AddDays(8))
.OfType<PageData>();
}
}</code></pre>
<p>So to recap on the job here. </p>
<p>I first get a list of all pages that are expiring in the next 7 days. The method "GetExpiringPages" just returns a list of pages that are about to be expired and that is the list of pages I look through. I check to see if the notification has already been sent for this current page. If the notification hasn't been sent, then i create a new postnotification to the creator of the page and send them a message. </p>
<p><img src="/link/ea12dd67e1274ab3a915e2fe7d6d2f5f.aspx" /></p>
<p>I hope this helps someone who might need a sample of using epi notifications.</p>Episerver Form MailChimp Integration/blogs/Joshua-Folkerts/Dates/2019/2/episerver-form-mailchimp-integration/2019-02-12T01:04:44.0000000Z<p><span style="font-weight: 400;">Episerver Forms are maturing with every update, which helps both developers and those in marketing. But while most people choose to use Mailchimp’s embedded forms to </span><span style="font-weight: 400;">capture</span><span style="font-weight: 400;"> mailing lists, I wanted to allow EPiServer editors to </span><span style="font-weight: 400;">integrate</span><span style="font-weight: 400;"> with a Mailchimp list. In this post, I am going to walk you through how I implemented Mailchimp into Episerver’s forms and allowed Episerver to post data via Mailchimp’s API.</span></p>
<p><span style="font-weight: 400;">Episerver provides an extensive API for forms and integration points, but the question remained: how easy is it to really tie into Episerver’s forms? In practice, it’s pretty easy — but once I dove in I found the documentation just isn’t mature enough, so I started decompiling assemblies to see what I could leverage.</span></p>
<p><span style="font-weight: 400;">(Note: this example is for Mailchimp, but this process really holds true for </span><span style="font-weight: 400;">any</span><span style="font-weight: 400;"> integration that provides an API that can be used to fetch information.)</span></p>
<h2>Methodology Preview</h2>
<p><span style="font-weight: 400;">First, we will provide a service that will fetch information from Mailchimp.</span></p>
<p><span style="font-weight: 400;">Next, we will create a scheduled job to cache the list and its properties, so editors don't need to wait for the API calls to request information, making for a better editing experience.</span></p>
<p><span style="font-weight: 400;">From here, we will tie into EPiServer’s forms API and create an IExternalSystem to allow editors to choose a mailing list to submit data to.</span></p>
<p><span style="font-weight: 400;">Finally, we will create a PostSubmissionActor to post data to Mailchimp.</span></p>
<p><span style="font-weight: 400;">I have used this methodology for a couple of projects, and it seems to work well.</span></p>
<h2>Create a Service and Scheduled Job</h2>
<p><span style="font-weight: 400;">With all that said, let's first start by installing the Mailchimp NuGet package: “MailChimp.Net.V3”. This will provide the means to retrieve the lists and fields from Mailchimp.</span></p>
<ol>
<li><span style="font-weight: 400;">Install</span><span style="font-weight: 400;">-</span><span style="font-weight: 400;">Package</span> <span style="font-weight: 400;">MailChimp</span><span style="font-weight: 400;">.</span><span style="font-weight: 400;">net</span><span style="font-weight: 400;">.</span><span style="font-weight: 400;">V3</span></li>
</ol>
<p><span style="font-weight: 400;">After installing the package, confirm you have the latest version of Episerver Forms. (As of publish date, this was version 4.22). Then install or update Episerver forms to your project</span></p>
<ol>
<li><span style="font-weight: 400;">Install</span><span style="font-weight: 400;">-</span><span style="font-weight: 400;">Package</span> <span style="font-weight: 400;">EPiServer</span><span style="font-weight: 400;">.</span><span style="font-weight: 400;">Forms</span></li>
</ol>
<p><span style="font-weight: 400;">Once both of these packages are installed, create the Mailchimp service. This service will be used in the scheduled job, provide the datasource for the forms, and be the post-submission actor. You’ll see the service is pretty straight forward: it’s more of a class used to fetch information and one method to submit the data to Mailchimp.</span></p>
<p><span style="font-weight: 400;">(You will notice there is a using statement for Nito.AsyncEx, which is a helper class for calling async methods in a non async method. You can also use GetAwaiter(), but for this tutorial we have chosen Nito.)</span></p>
<p><span style="font-weight: 400;">Here is what the MailChimpService.cs file looks like:</span></p>
<pre class="language-csharp"><code>using EPiServer.ServiceLocation;
using MailChimp.Net;
using MailChimp.Net.Interfaces;
using MailChimp.Net.Models;
using MailChimpSample.Business.Caching;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
namespace MailChimpSample.Business.MailChimpAPI
{
[ServiceConfiguration(ServiceType = typeof(MailChimpService), Lifecycle = ServiceInstanceScope.Transient)]
public class MailChimpService
{
public readonly IMailChimpManager mailChimpManager;
readonly ICacheService cacheService;
public const int CacheTimeout = 720; // in minutes
public MailChimpService(ICacheService cacheService)
{
this.cacheService = cacheService;
this.mailChimpManager = new MailChimpManager(ConfigurationManager.AppSettings["MailChimpApiKey"]);
}
public List<List> GetLists() => this.cacheService.Get(MailChimpConstants.ListIds, CacheTimeout, () =>
{
return AsyncContext.Run(() =>
this.mailChimpManager.Lists
.GetAllAsync())
.OrderBy(x => x.Name)
.ToList();
});
public Dictionary<string, string> GetListsAsDictionary()
{
return this.cacheService.Get(MailChimpConstants.ListDictionaryItems, CacheTimeout, () =>
{
var list = new Dictionary<string, string>();
var items = this.GetLists();
foreach (var item in items)
{
if (!list.ContainsKey(item.Id))
{
list.Add(item.Id, item.Name);
}
}
return list;
});
}
public List GetListByName(string name)
{
string cacheKey = string.Format(MailChimpConstants.ListId, name);
return this.cacheService.Get(cacheKey, CacheTimeout, () =>
{
var result = this.GetLists().FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
return result;
});
}
public List GetListById(string id)
{
string cacheKey = string.Format(MailChimpConstants.ListId, id);
return this.cacheService.Get(cacheKey, CacheTimeout, () =>
{
return AsyncContext.Run(() =>
this.mailChimpManager.Lists.GetAsync(id.ToString()));
});
}
public List<MergeField> GetFormFields(string listId) => this.cacheService.Get(string.Format(MailChimpConstants.ListMergeFields, listId), CacheTimeout, () =>
{
var mergeFields = AsyncContext.Run(() =>
this.mailChimpManager.MergeFields
.GetAllAsync(listId.ToString()))
.ToList();
return mergeFields;
});
public Dictionary<string, string> GetFormFieldsAsDictionary(string listId)
{
var dictionary = new Dictionary<string, string>() { { "EMAIL", "Email" } };
this.GetFormFields(listId)
.Select(x => new KeyValuePair<string, string>(x.Tag, x.Name))
.ToList()
.ForEach(x => dictionary.Add(x.Key, x.Value));
return dictionary;
}
public Member Send(string listId, Dictionary<string, string> fields)
{
var externalFields = this.GetFormFields(listId);
var member = new Member()
{
EmailAddress = fields["EMAIL"],
StatusIfNew = Status.Subscribed
};
foreach (var externalField in externalFields)
{
if (fields.ContainsKey(externalField.Tag))
member.MergeFields.Add(externalField.Tag, fields[externalField.Tag]);
}
return AsyncContext.Run(() =>
this.mailChimpManager.Members.AddOrUpdateAsync(listId.ToString(), member));
}
}
}</code></pre>
<p><span style="font-weight: 400;">This file contains some extra methods that are not used, but are included in case you need to get a list by name or id. Additionally, you’ll see a caching mechanism — the source includes an abstract class to roll your own cache, but we use the ISynchronizedObjectInstanceCache along with an interface as well.</span></p>
<h2>Create External Datasource</h2>
<p><span style="font-weight: 400;">Now that we have defined our service, we need to create an </span>ExternalDatasource<span style="font-weight: 400;"> for Episerver forms to populate the forms mappings dropdown list. This allows the editor to choose a mapping for submitting a form to a list, and also allows them to map form fields to the Mailchimp list field. We essentially are creating a one-to-one mapping from Episerver Form Field to Mailchimp Form Field.</span></p>
<p><span style="font-weight: 400;">Below is the IExternalDatasource that allows us to setup the datasources for the mappings.</span></p>
<pre class="language-csharp"><code>using EPiServer.Forms.Core;
using EPiServer.Forms.Core.Internal.Autofill;
using EPiServer.Forms.Core.Internal.ExternalSystem;
using EPiServer.ServiceLocation;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MailChimpSample.Business.MailChimpAPI
{
public class MailChimpDataSystem : IExternalSystem, IAutofillProvider
{
readonly MailChimpService mailChimpService;
public MailChimpDataSystem()
{
this.mailChimpService = ServiceLocator.Current.GetInstance<MailChimpService>();
}
public virtual string Id =>
"MailChimpDataSystem";
public virtual IEnumerable<IDatasource> Datasources
{
get
{
var items = this.mailChimpService.GetListsAsDictionary();
var datasources = items
.Select(x => new Datasource()
{
Name = x.Value,
Id = x.Key,
OwnerSystem = this,
Columns = this.mailChimpService.GetFormFieldsAsDictionary(x.Key)
});
return datasources;
}
}
public IEnumerable<string> GetSuggestedValues(IDatasource selectedDatasource, IEnumerable<RemoteFieldInfo> remoteFieldInfos, ElementBlockBase content, IFormContainerBlock formContainerBlock, HttpContextBase context)
{
if (selectedDatasource == null || remoteFieldInfos == null)
Enumerable.Empty<string>();
if (this.Datasources.Any(ds => ds.Id == selectedDatasource.Id) // datasource belong to this system
&& remoteFieldInfos.Any(mi => mi.DatasourceId == selectedDatasource.Id)) // and remoteFieldInfos is for our system datasource
{
return Enumerable.Empty<string>();
}
return Enumerable.Empty<string>();
}
}
}</code></pre>
<p><span style="font-weight: 400;">Let’s break this down a bit.</span></p>
<ul>
<li>Id field<span style="font-weight: 400;"> tells the system the ID of this datasource.</span></li>
<li>datasources<span style="font-weight: 400;"> is the list of items that are keys for the datasource, which returns an Ienumerable of datasources. In the example above, it is going to return </span><span style="font-weight: 400;">all lists</span><span style="font-weight: 400;"> in MailChimp.</span></li>
<li>suggested values<span style="font-weight: 400;"> allows the system to change the datasources to match the autocomplete on — you’ll notice in our example this is the same as datasources</span></li>
</ul>
<p><span style="font-weight: 400;">Now that we have the sources set up, we can then open Episerver Forms and start mapping. In this example, we are caching the items via a scheduled job. This is one method: we can use a traditional method as it will cache at first call, but that will take a couple seconds as the system populates the first time, so we suggest creating a scheduled job that primes the cache for the forms. This way there is no need to call out to Mailchimp on load, as it is already loaded.</span></p>
<p><span style="font-weight: 400;">Below is a sample of my scheduled job.</span></p>
<pre class="language-csharp"><code>using EPiServer.PlugIn;
using EPiServer.Scheduler;
using EPiServer.ServiceLocation;
using MailChimpSample.Business.Caching;
using System;
namespace MailChimpSample.Business.MailChimpAPI
{
[ScheduledPlugIn(DisplayName = "Mail Chimp Cache Manager", IntervalType = EPiServer.DataAbstraction.ScheduledIntervalType.Hours, Restartable = true, IntervalLength = 6)]
public class MailChimpCacheScheduledJob : ScheduledJobBase
{
private bool _stopSignaled;
readonly MailChimpService mailchimpService;
readonly ICacheService cacheService;
public MailChimpCacheScheduledJob()
{
IsStoppable = true;
this.mailchimpService = ServiceLocator.Current.GetInstance<MailChimpService>();
this.cacheService = ServiceLocator.Current.GetInstance<ICacheService>();
}
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
_stopSignaled = true;
}
/// <summary>
/// Called when a scheduled job executes
/// </summary>
/// <returns>A status message to be stored in the database log and visible from admin mode</returns>
public override string Execute()
{
//Call OnStatusChanged to periodically notify progress of job for manually started jobs
OnStatusChanged(String.Format("Starting execution of {0}", this.GetType()));
// Clear Lists
this.cacheService.Remove(MailChimpConstants.ListIds);
var lists = this.mailchimpService.GetLists();
var cacheItemsUpdated = 0;
foreach (var list in lists)
{
if (_stopSignaled)
{
return "Stop of job was called";
}
this.cacheService.Remove(string.Format(MailChimpConstants.ListId, list.Id));
this.cacheService.Get(string.Format(MailChimpConstants.ListId, list.Id), 720, () =>
{
return list;
});
this.cacheService.Remove(string.Format(MailChimpConstants.ListMergeFields, list.Id));
this.mailchimpService.GetFormFields(list.Id);
cacheItemsUpdated++;
this.OnStatusChanged($"Cache Items Updated: {cacheItemsUpdated}");
}
// Clear the dictionary
this.cacheService.Remove(MailChimpConstants.ListDictionaryItems);
this.mailchimpService.GetListsAsDictionary();
//For long running jobs periodically check if stop is signaled and if so stop execution
return $"Cache Items Updated: {cacheItemsUpdated}";
}
}
}</code></pre>
<p><span style="font-weight: 400;"> </span></p>
<p><span style="font-weight: 400;">You can see that we are grabbing all the Mailchimp lists, then iterating those lists to grab all the associated fields.</span></p>
<h2>The Editor Interface </h2>
<p><span style="font-weight: 400;">Now that we have the Mailchimp service and DataSource created, and we’ve created a scheduled Job to prime the cache, let's explore how this implementation will look to the editor.</span></p>
<p><span style="font-weight: 400;">First, create a form in Episerver. As an example, I have added a simple form to the Alloy Demo, with three text properties (FirstName, LastName, EmailAddress) and a submit button.</span><span style="font-weight: 400;"><br /></span></p>
<p><span style="font-weight: 400;"><img src="/link/6fbfbf5351c6480a9717a14c27cbaa92.aspx" /></span></p>
<p><span style="font-weight: 400;">Then, we map this form to a Mailchimp list. You’ll notice there’s now a “Mappings” tab in the form container block, which displays as soon as you create a class that implements IExternalSystem.</span></p>
<p><span style="font-weight: 400;">This dropdown is filled from your IEnumerable<IDatasource> Datasources property from the MailChimpDataSystem.cs file we created above. You should see a screen that looks like the following, except with your Mailchimp Lists in the dropdown.</span></p>
<p><span style="font-weight: 400;"><img src="/link/80250f21aa0c4d0595169ac40b86fd1f.aspx" /></span></p>
<p><span style="font-weight: 400;">In this sample, we will select “Episerver Forms Sample,” which tells Episerver that we want to map that form to the Mailchimp list. Once you publish the form, you’ll need to tie those form elements to the fields in our Mailchimp list, which again are pulled from the datasources “Columns” Field on the selected datasource in the image above.</span></p>
<p><span style="font-weight: 400;"><img src="/link/65bc0e0b9ee24fecba762218145e0fb3.aspx" /></span></p>
<p><span style="font-weight: 400;">You can see that the list is expecting a set of fields — in this example, remember we used First Name, Last name, and Email. (NOTE: Email is required in Mailchimp, so keep that in mind when using Mailchimp integration in Episerver)</span></p>
<p><span style="font-weight: 400;">In this example, we will select First name, and then we will publish the form.</span></p>
<h3>Including PostSubmissionActor</h3>
<p><span style="font-weight: 400;">We now have a form that is mapped one-to-one with Mailchimp, but one last thing is missing: when a user submits the form, how will data </span><span style="font-weight: 400;">be sent</span><span style="font-weight: 400;"> to Mailchimp?</span></p>
<p><span style="font-weight: 400;">This is where the PostSubmissionActor comes into play. To wrap all of this up, we need to create a form submission actor that will handle a few different things:</span></p>
<ul>
<li><span style="font-weight: 400;">First, we need to convert all the mappings and form fields to a dictionary.</span></li>
<li><span style="font-weight: 400;">Then, we review the ActiveExternalFieldMappingTable and loop the list fields to see if the activeexternalfieldmappingtable contains the same property name as the Mailchimp list field. If the field doesn’t exist, it is ignored.</span></li>
<li><span style="font-weight: 400;">Finally, once all of the mapped fields have been added to the dictionary, we send it off to the MailChimpService to send to Mailchimp. </span></li>
</ul>
<pre class="language-csharp"><code>using EPiServer.Forms.Core.PostSubmissionActor;
using EPiServer.ServiceLocation;
using System.Collections.Generic;
namespace MailChimpSample.Business.MailChimpAPI
{
public class MailChimpPostActor : PostSubmissionActorBase
{
readonly MailChimpService mailChimpService;
public MailChimpPostActor()
{
this.mailChimpService = ServiceLocator.Current.GetInstance<MailChimpService>();
}
public override object Run(object input)
{
string submissionResult = string.Empty;
if (this.SubmissionData == null)
return submissionResult;
Dictionary<string, string> postedFormDataDictionary = new Dictionary<string, string>();
foreach (KeyValuePair<string, object> pair in this.SubmissionData.Data)
if (!pair.Key.ToLower().StartsWith("systemcolumn") && pair.Value != null)
postedFormDataDictionary.Add(pair.Key, pair.Value.ToString());
var mappings = base.ActiveExternalFieldMappingTable;
if (mappings != null)
{
Dictionary<string, string> formDataAttributes = new Dictionary<string, string>();
string listId = string.Empty;
foreach (var item in mappings)
{
if (item.Value != null)
{
var fieldName = item.Key;
var remoteFieldName = item.Value.ColumnId;
if (postedFormDataDictionary.ContainsKey(fieldName))
{
formDataAttributes.Add(remoteFieldName, postedFormDataDictionary[fieldName]);
if (string.IsNullOrWhiteSpace(listId))
listId = item.Value.DatasourceId;
}
}
}
if (formDataAttributes.Count > 0)
this.mailChimpService.Send(listId, formDataAttributes);
}
return submissionResult;
}
}
}</code></pre>
<p><span style="font-weight: 400;">The final test is to fill out a form and hit submit. If that record exists in Mailchimp and is mapped correctly based on our submission, then we can consider this a success!</span></p>
<p><span style="font-weight: 400;"><img src="/link/977ad93f6f2a4c81944ef9c4db49b468.aspx" /></span></p>
<h2>Recap</h2>
<p><span style="font-weight: 400;">Let’s recap what we accomplished here. </span></p>
<ol>
<li><span style="font-weight: 400;">We </span>created a mailchimp service<span style="font-weight: 400;"> that handles the communication to Mailchimp’s API</span></li>
<li><span style="font-weight: 400;">We </span>created a Datasource<span style="font-weight: 400;"> that talks to the Mailchimp service to retrieve lists and fields from Mailchimp</span></li>
<li><span style="font-weight: 400;">We </span>created a new scheduled job<span style="font-weight: 400;"> to prime the cache so there’s no need to wait for items to be returned</span></li>
<li><span style="font-weight: 400;">We </span>created a PostSubmissionActor<span style="font-weight: 400;"> to handle mappings from Episerver to Mailchimp fields and submitted those to Mailchimp</span></li>
<li><span style="font-weight: 400;">We </span>submitted the new form<span style="font-weight: 400;"> and tested the results in Mailchimp’s dashboard.</span></li>
</ol>
<p><span style="font-weight: 400;">In this example, I used Mailchimp, but you can implement this with any third party service such as Silverpop, Exact Target, or other API-based service. I chose Mailchimp for this example as it was easier to create a new test account than the other systems.</span></p>
<h2><span style="font-weight: 400;">Code for Project</span></h2>
<p><a href="https://gist.github.com/folkertsj/c1b16cb36b24174a841ff52aca10bf0c">Download Code</a></p>Page Type Tabs For EPiServer/blogs/Joshua-Folkerts/Dates/2011/8/Page-Type-Tabs-Available-For-EPiServer/2011-08-10T14:25:00.0000000Z<p>Since I started developing in EPiServer, I have always found how nicely written their framework is.  One thing i have noticed though, is the fact that you really can't hook into their "Create New", "Delete Page" pages.  I have also had an issue that there is not a way to group PageTypes in the admin mode or Create new screen.</p> <p>I have been working on a project at work where there are a boat load of PageTypes and it has become quite tiresome searching for the right PageType.  You know the PageType exists, you just can't find it in the list.  So I decided to dig into the create new page and find a way to group these PageTypes and allow searching as well in case they do not want to use the tabs.  The only issue that i have found so far is there is no admin/edit interface to group PageTypes but that is okay with me since i am a coder.  I have although created the same thing without using <a href="http://pagetypebuilder.codeplex.com/">PageTypeBuilder</a> but as mentioned earlier, I am a coder so I chose to use this blog and spend more time with this project. </p> <h2>Using PageTypeTabs</h2> <p>In order to use PageTypeTabs, you will need to make sure you have reference PageTypeBuilder in your project.  This code does use PageTypeBuilder to find the proper PageTypes based on the classes we marked with a PageTypeTab Attribute.</p> <h2>Creating a Tab</h2> <pre><div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper"><div style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum1"> 1:</span> <span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> NewTab : PageTypeTab</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum2"> 2:</span> {</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum3"> 3:</span> <span style="color: #0000ff">public</span> <span style="color: #0000ff">override</span> <span style="color: #0000ff">string</span> Name</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum4"> 4:</span> {</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum5"> 5:</span> get { <span style="color: #0000ff">return</span> <span style="color: #006080">"New Tab"</span>; } <span style="color: #008000">// name of tab in ui</span></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum6"> 6:</span> }</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum7"> 7:</span>  </pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum8"> 8:</span> <span style="color: #0000ff">public</span> <span style="color: #0000ff">override</span> <span style="color: #0000ff">int</span> SortIndex</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum9"> 9:</span> {</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum10"> 10:</span> get { <span style="color: #0000ff">return</span> 300; } <span style="color: #008000">// default is 100</span></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum11"> 11:</span> }</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum12"> 12:</span> }</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum13"> 13:</span>  </pre><!--CRLF--></div></div><p> </p></pre>
<h2>Adding PageTypeTab Attribute To Our PageType Class</h2>
<p>Now that we have our PageTypeTab created, we need to assign it to a PageType by adding an attribute to the PageType class we created using PageTypeBuilder. Below, you will see find the syntax of adding the attribute to the class. </p>
<pre><div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper"><div style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum1"> 1:</span> [PageTypeTab( Tab=<span style="color: #0000ff">typeof</span>(NewTab))]</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum2"> 2:</span> [PageType(Filename = <span style="color: #006080">"/NewPage.aspx"</span>, Name = <span style="color: #006080">"Album Page"</span>, AvailableInEditMode = <span style="color: #0000ff">true</span>, Description = <span style="color: #006080">"This pagetype is of NewPage."</span>)]</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum3"> 3:</span> <span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> NewPage : TypedPageData</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum4"> 4:</span> {</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum5"> 5:</span> }</pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum6"> 6:</span>  </pre><!--CRLF--></div></div></pre>
<h2>Adding Virtual Path Mapping</h2>
<p>Now that wasn't so hard now was it?  It is the same process as adding a tab to a PageTypeProperty in PageTypeBuilder.  This isn't by accident, I used some of Joel's framework architecture to make it as common as possible so you don't have to do it another way all the time.</p>
<p>There is one last step to do and that is tap into the CreateNew page URL and show our page instead of EPiServer’s page.  The following code will need to be added to the episerver.config file in order for us to intercept the CreateNew page URL. </p>
<pre><div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper"><div style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum1"> 1:</span> <virtualPath customFileSummary=<span style="color: #006080">"~/FileSummary.config"</span>></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum2"> 2:</span> <providers></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum3"> 3:</span> <clear /></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum4"> 4:</span> <add showInFileManager=<span style="color: #006080">"false"</span> virtualName=<span style="color: #006080">"CreateNewPage"</span> virtualPath=<span style="color: #006080">"~/Templates/PageTypeTemplates/Overrides/CreateNewPage.aspx"</span> bypassAccessCheck=<span style="color: #006080">"false"</span> physicalPath=<span style="color: #006080">""</span> name=<span style="color: #006080">"CreateNewPageMapping"</span> type=<span style="color: #006080">"EPiServer.Web.Hosting.VirtualPathMappedProvider,EPiServer"</span> /></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum5"> 5:</span> </providers></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum6"> 6:</span> <filters /></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum7"> 7:</span> </virtualPath></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum8"> 8:</span> <!-- virtualPathMappings are used by <span style="color: #006080">"VirtualPathMappedProvider"</span>. --></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum9"> 9:</span> <virtualPathMappings></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum10"> 10:</span> <add url=<span style="color: #006080">"~/secure/CMS/Edit/NewPage.aspx"</span> mappedUrl=<span style="color: #006080">"~/Templates/PageTypeTemplates/Overrides/CreateNewPage.aspx"</span> /></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: white; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum11"> 11:</span> </virtualPathMappings></pre><!--CRLF--><pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px"><span style="color: #606060" id="lnum12"> 12:</span>  </pre><!--CRLF--></div></div></pre>
<h2>What We've Achieved (Search built in)</h2>
<p><a href="/link/949f715aad1c4253810f232b9a6f0a73.gif"><img style="background-image: none; border-right-width: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="FinalLookOfPageTypeTab" border="0" alt="FinalLookOfPageTypeTab" src="/link/01c739503c3d4b78bc97ca909bbbb299.gif" width="244" height="97" /></a></p>
<p><a href="/link/d1daea18c5514347ac802552c26422f2.gif"><img style="background-image: none; border-right-width: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="FinalLookOfPageTypeTab_2" border="0" alt="FinalLookOfPageTypeTab_2" src="/link/c2bf362b028b4b0ab647de9936be1bb7.gif" width="244" height="77" /></a></p>
<p><a href="/link/e6d4f0b4a6dd4392bad447f38b078676.gif"><img style="background-image: none; border-right-width: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px; padding-top: 0px" title="PageTypeTabSearch" border="0" alt="PageTypeTabSearch" src="/link/75f68dd1374b46a68b576857244edd6e.gif" width="244" height="84" /></a></p>
<h2>Get The Files</h2>
<div style="padding-bottom: 0px; margin: 0px; padding-left: 0px; padding-right: 0px; display: inline; float: none; padding-top: 0px" id="scid:8eb9d37f-1541-4f29-b6f4-1eea890d4876:e332466a-f595-4631-9ecd-6e0279a52cb2" class="wlWriterSmartContent">
<div><a href="/link/af53f22e8eeb4c438c8a8553f73abc9f.zip" target="_self">PageTypeTabs.zip</a></div>
</div>