Migrating Sitefinity 4 Modules to Dynamic Module Builder

By March 15, 2012Sitefinity

There has been many features released in Sitefinity within the last year, but one of my most favorite is Module Builder! If you have read my previous blog post about migrating modules from Sitefinity 3.7 to 4.0, you must have been overwhelmed with the 8000+ lines of code it took to create a module in Sitefinity 4. And most of the it was cookie-cutter code from module to module.

Now with Sitefinity 4 and 5’s Module Builder, you can create modules without writing any code! If you have manually created modules for Sitefinity 4, you should be asking yourself how you can move it into the module builder at this point. Why risk future upgrades breaking your old module or not being able to leverage the built-in API features of Dynamic Modules. And the best part: any new features released for Dynamic Modules is a new feature released for your custom module.

Below I am going to show you how to migrate your old module data into the new module builder. It is easier than you may think. First create your new content type through the Moodule Builder. Be sure to create the same fields that are in your old module. Next create an aspx page in our Sitefinity solution called “Migrate.aspx”. We are going to use the code-behind for putting our migration logic. Code is worth a thousand words so here it is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using OldModule.Data;
using Telerik.Sitefinity.DynamicModules;
using Telerik.Sitefinity.Model;
using Telerik.Sitefinity.Modules.Libraries;
using Telerik.Sitefinity.Security;
using Telerik.Sitefinity.Utilities.TypeConverters;
using Telerik.Sitefinity.Workflow;
 
namespace SitefinityWebApp.Custom
{
    public partial class Migration : System.Web.UI.Page
    {
        public const string DynamicType = "Telerik.Sitefinity.DynamicTypes.Model.Customitems.CustomItem";
        public const string UrlNameCharsToReplace = @"[^\w\-\!\$\'\(\)\=\@\d_]+";
        public const string UrlNameReplaceString = "-";
 
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                txtDynamicType.Text = DynamicType;
            }
        }
 
        protected void btnDisplay_Click(object sender, EventArgs e)
        {
            var manager = OldModule.Data.OldManager.GetManager();
            var items = manager.GetOldItems();
            var query = from item in items.ToList()
                        select new
                        {
                            Title = item.GetValue<Lstring>("Title").Value,
                            Description = item.Content.Value,
                            MediumImage = item.GetValue<string>("MediumImage"),
                            ThumbnailImage = item.GetValue<string>("ThumbnailImage"),
                            PublicationDate = item.PublicationDate
                        };
            grdItems.DataSource = query;
            grdItems.DataBind();
        }
 
        protected void btnMigrate_Click(object sender, EventArgs e)
        {
            var manager = OldManager.GetManager();
            var items = manager.GetOldItems();
            var query = from item in items.ToList()
                        select new
                                    {
                                        Title = item.GetValue<Lstring>("Title").Value,
                                        Description = item.Content.Value,
                                        MediumImage = item.GetValue<string>("MediumImage"),
                                        ThumbnailImage = item.GetValue<string>("ThumbnailImage"),
                                        PublicationDate = item.PublicationDate,
                                        Owner = item.Owner,
                                        UrlName = item.UrlName
                                    };
 
            //begin migration to dynamic module
            using (var dynamicModuleManager = DynamicModuleManager.GetManager())
            {
                DateTime current = DateTime.Now;
                int i = 0;
 
                //migrate each legacy item to module builder
                foreach (var item in query)
                {
                    var succeed = CreateDynamicItems(dynamicModuleManager, item.Title, item.Description, item.MediumImage, item.ThumbnailImage, item.PublicationDate, item.Owner, item.UrlName);
 
                    //skip if not imported
                    if (!succeed)
                        continue;
                     
                    //stop if limit reached (usually for testing)
                    if (!string.IsNullOrWhiteSpace(txtMigrationLimit.Text) && ++i >= Convert.ToInt32(txtMigrationLimit.Text))
                        break;
                }
 
                //display status
                lblMessage.Text = string.Format("{0} items migrated in {1} seconds", i, (DateTime.Now - current).Duration().Seconds);
 
            }
        }
         
        //creates a new dynamic item
        public bool CreateDynamicItems(DynamicModuleManager dynamicModuleManager, string title, string description, string mediumImage, string thumbnailImage, DateTime publicationDate, Guid owner, string urlName)
        {
            var dynamicType = TypeResolutionService.ResolveType(txtDynamicType.Text);
            var dynamicItem = dynamicModuleManager.GetDataItems(dynamicType)
                .FirstOrDefault(i => i.GetValue<string>("Title") == title);
 
            if (dynamicItem == null)
            {
                dynamicItem = dynamicModuleManager.CreateDataItem(dynamicType);
 
                // This is how values for the properties are set
                dynamicItem.SetValue("Title", title);
                dynamicItem.SetValue("Owner", SecurityManager.GetCurrentUserId());
                dynamicItem.SetValue("Description", description);
                dynamicItem.PublicationDate = publicationDate;
                dynamicItem.Owner = owner;
                dynamicItem.UrlName = urlName;
 
                var libManager = LibrariesManager.GetManager();
                var list = libManager.GetImages().ToList();
                //can manager.GetItemFromUrl improve below?
 
                //get medium image
                var itemMed = list.FirstOrDefault(i => ScrubMediaPath(i.MediaUrl) == ScrubMediaPath(mediumImage));
                if (itemMed != null)
                {
                    dynamicItem.AddImage("MediumImage", itemMed.Id);
                }
 
                //get thumbnail image
                var itemTmb = list.FirstOrDefault(i => ScrubMediaPath(i.MediaUrl) == ScrubMediaPath(thumbnailImage));
                if (itemTmb != null)
                {
                    dynamicItem.AddImage("ThumbnailImage", itemTmb.Id);
                }
 
                //save to database
                dynamicModuleManager.SaveChanges();
 
                //Publish the news item. The published version acquires new ID.
                var bag = new Dictionary<string, string>();
                bag.Add("ContentType", dynamicType.FullName);
                WorkflowManager.MessageWorkflow(dynamicItem.Id, dynamicType, null, "Publish", false, bag);
 
                return true;
            }
 
            return false;
        }
 
        protected string ScrubMediaPath(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
                return null;
            Uri uri;
            if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out uri))
                return null;
            if (uri.IsAbsoluteUri)
                path = uri.LocalPath;
            if (VirtualPathUtility.IsAppRelative(path))
                path = VirtualPathUtility.ToAbsolute(path);
 
            if (path.Length <= 1)
                return null;
 
            path = path.Substring(1);
 
            int indx = path.LastIndexOf('.');
            if (indx != -1)
            {
                string extension = path.Substring(indx);
                if (extension.StartsWith(".tmb"))
                {
                    path = path.Substring(0, indx);
                }
 
                if (!path.StartsWith("/"))
                    path = "/" + path;
 
                return path.ToLower();
            }
            return null;
        }
    }
}

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Migration.aspx.cs" Inherits="SitefinityWebApp.Custom.Migration" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="lblMessage" runat="server" Font-Bold="True" ForeColor="Red"></asp:Label><br/><br/>
        Enter the name of the dynamic type:<br/>
        <asp:TextBox runat="server" ID="txtDynamicType" Width="500"></asp:TextBox><br/><br/>
        Enter max number of items to migrate (use small number for testing):<br/>
        <asp:TextBox runat="server" ID="txtMigrationLimit"></asp:TextBox><br/><br/>
        <asp:Button ID="btnDisplay" runat="server" Text="Display" onclick="btnDisplay_Click" />
        <asp:Button ID="btnMigrate" runat="server" Text="Migrate" onclick="btnMigrate_Click" /><br/><br/>
        <asp:GridView ID="grdItems" runat="server">
        </asp:GridView>
    </div>
    </form>
</body>
</html>

As you can see, the interface is constructed of a “Display” and “Migrate” button. You can click “Display” just to show a grid of your old items. This will just give you an idea of what you are going to migrate. Enter the correct name for your custom module type and a limit of how many to migrate if you like. Once that is done, click “Migrate” and you should see something like this:

To explain the code a bit, we are simply reading data out of the old module and creating the items into the dynamic module. That’s it! What is even nice is that the code for creating, retriving, updating, and deleteing content in your dynamic module is included for you right from the admin interface:

The migration code will have to be updated to reflect your own module and its custom fields. Also you may notice the process takes some time if you have many content items in your old module, but this code only runs once for the migration and then you will not have to use it again. I hope this helps!

Update: Looks like Dynamic Modules will get multi-language support in 5.1 (multilingual custom fields!): http://www.sitefinity.com/devnet/forums/sitefinity-4-x/developing-with-sitefinity/4-4-module-builder—multi-language-support.aspx

Happy Coding!!

The following two tabs change content below.