Monday, April 7, 2014

Problem with cross site publishing catalog connections when use SPExport/SPImport for copying sites

In my blog I already wrote about various problems on sites after export/import (see e.g. Using SPExport/SPImport for copying content with managed metadata or Problem with SPContextPageInfo.IsWebWelcomePage on imported Sharepoint sites). There was also article about problems with connecting to catalogs on imported sites: Problem with connecting to catalog on site collections imported with SPExport/SPImport API in Sharepoint. In this post I will describe one more problem with publishing catalog connections on imported sites.

Suppose that we have site collection which uses cross-site publishing, i.e. has one or more connected catalogs. After that we export site collection via SPExport and import it to another location via SPImport. Now if we will check available catalog connections on the imported site in Site settings > Manage catalog connections there won’t be any connections. However if we will click Connect to catalog on the same page we will see all connections from the source site. The problem is that they still will point to the source site and it won’t be possible to disconnect them from UI (in the last column where normally there is Connect link, there will be server-relative URL of the source site collection which won’t be clickable).

In order to solve this problem we discussed the following approach with colleagues: if it is not possible to disconnect copied connections from UI, we will try to do it programmatically, and after that will check will it be possible to reconnect them from the target site. My colleague Mikko Niemi created console utility which enumerates all available connections and allows to disconnect them one by one. With his approval I post the code of this utility here:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         var siteUrl = args[0];
   6:  
   7:         Console.WriteLine("Trying to connect site '" + siteUrl + "'...");
   8:  
   9:         using (var site = new SPSite(siteUrl))
  10:         {
  11:             Console.ForegroundColor = ConsoleColor.Green;
  12:             Console.WriteLine("Connected to site '" + site.Url + "'");
  13:             Console.WriteLine();
  14:             Console.ForegroundColor = ConsoleColor.Gray;
  15:  
  16:             while (true)
  17:             {
  18:                 ShowAndDeleteConnections(site);
  19:  
  20:                 Console.WriteLine("To delete more press enter or " +
  21:                     "type q to quit:");
  22:                 var line = Console.ReadLine().ToLower();
  23:                 if (line == "q" || line == "quit")
  24:                 {
  25:                     break;
  26:                 }
  27:             }
  28:  
  29:         }
  30:  
  31:         Console.WriteLine();
  32:         Console.WriteLine("Finished, press key to close!");
  33:         Console.ReadKey();
  34:     }
  35:  
  36:     private static void ShowAndDeleteConnections(SPSite site)
  37:     {
  38:         Console.WriteLine("Fetching catalog connections... ");
  39:  
  40:         var catalogConnectionManager = new CatalogConnectionManager(site);
  41:         var catalogs = catalogConnectionManager.ConnectedPublishingCatalogs;
  42:  
  43:         if (catalogs == null || !catalogs.Any())
  44:         {
  45:             Console.ForegroundColor = ConsoleColor.Yellow;
  46:             Console.WriteLine("No connected catalog connections found!");
  47:             Console.ForegroundColor = ConsoleColor.Gray;
  48:         }
  49:         else
  50:         {
  51:             Console.WriteLine("Connected catalogs:");
  52:             var i = 1;
  53:             foreach (var c in catalogs)
  54:             {
  55:                 Console.ForegroundColor = ConsoleColor.White;
  56:                 Console.WriteLine("{0}. {1} ({2})", i, c.CatalogName,
  57:                     c.CatalogUrl);
  58:                 Console.WriteLine();
  59:                 Console.ForegroundColor = ConsoleColor.Gray;
  60:  
  61:                 i++;
  62:             }
  63:  
  64:             Console.WriteLine("Input the catalog number " +
  65:                 "that you want to delete (empty = no delete):");
  66:             var catalogIndex = Console.ReadLine();
  67:             if (!string.IsNullOrEmpty(catalogIndex))
  68:             {
  69:                 var catalogTodelete = catalogs[int.Parse(catalogIndex) - 1];
  70:                 Console.ForegroundColor = ConsoleColor.Yellow;
  71:                 Console.WriteLine("Deleting connection '" +
  72:                     catalogTodelete.CatalogName + "'...");
  73:                 Console.ForegroundColor = ConsoleColor.Gray;
  74:  
  75:                 catalogConnectionManager.
  76:                     DeleteCatalogConnection(catalogTodelete.CatalogUrl);
  77:                 catalogConnectionManager.Update();
  78:             }
  79:         }
  80:     }
  81: }
   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         var siteUrl = args[0];
   6:  
   7:         Console.WriteLine("Trying to connect site '" + siteUrl + "'...");
   8:  
   9:         using (var site = new SPSite(siteUrl))
  10:         {
  11:             Console.ForegroundColor = ConsoleColor.Green;
  12:             Console.WriteLine("Connected to site '" + site.Url + "'");
  13:             Console.WriteLine();
  14:             Console.ForegroundColor = ConsoleColor.Gray;
  15:  
  16:             while (true)
  17:             {
  18:                 ShowAndDeleteConnections(site);
  19:  
  20:                 Console.WriteLine("To delete more press enter or " +
  21:                     "type q to quit:");
  22:                 var line = Console.ReadLine().ToLower();
  23:                 if (line == "q" || line == "quit")
  24:                 {
  25:                     break;
  26:                 }
  27:             }
  28:  
  29:         }
  30:  
  31:         Console.WriteLine();
  32:         Console.WriteLine("Finished, press key to close!");
  33:         Console.ReadKey();
  34:     }
  35:  
  36:     private static void ShowAndDeleteConnections(SPSite site)
  37:     {
  38:         Console.WriteLine("Fetching catalog connections... ");
  39:  
  40:         var catalogConnectionManager = new CatalogConnectionManager(site);
  41:         var catalogs = catalogConnectionManager.ConnectedPublishingCatalogs;
  42:  
  43:         if (catalogs == null || !catalogs.Any())
  44:         {
  45:             Console.ForegroundColor = ConsoleColor.Yellow;
  46:             Console.WriteLine("No connected catalog connections found!");
  47:             Console.ForegroundColor = ConsoleColor.Gray;
  48:         }
  49:         else
  50:         {
  51:             Console.WriteLine("Connected catalogs:");
  52:             var i = 1;
  53:             foreach (var c in catalogs)
  54:             {
  55:                 Console.ForegroundColor = ConsoleColor.White;
  56:                 Console.WriteLine("{0}. {1} ({2})", i, c.CatalogName,
  57:                     c.CatalogUrl);
  58:                 Console.WriteLine();
  59:                 Console.ForegroundColor = ConsoleColor.Gray;
  60:  
  61:                 i++;
  62:             }
  63:  
  64:             Console.WriteLine("Input the catalog number " +
  65:                 "that you want to delete (empty = no delete):");
  66:             var catalogIndex = Console.ReadLine();
  67:             if (!string.IsNullOrEmpty(catalogIndex))
  68:             {
  69:                 var catalogTodelete = catalogs[int.Parse(catalogIndex) - 1];
  70:                 Console.ForegroundColor = ConsoleColor.Yellow;
  71:                 Console.WriteLine("Deleting connection '" +
  72:                     catalogTodelete.CatalogName + "'...");
  73:                 Console.ForegroundColor = ConsoleColor.Gray;
  74:  
  75:                 catalogConnectionManager.DeleteCatalogConnection(
  76:                     catalogTodelete.CatalogUrl);
  77:                 catalogConnectionManager.Update();
  78:             }
  79:         }
  80:     }
  81: }

With this utility you may disconnect copied “broken” connections and reconnect them again e.g. from UI (after disconnecting near appropriate connection you will see Connect link). Hope that it will help you if you will face with similar problem.

Wednesday, April 2, 2014

Sharepoint MVP 2014

Yesterday I got an email from MS about renewal of my MVP award for 2014 year. This is my 4th award starting from 2011 and each year had something specific. Last year I wrote a lot of posts and articles about new version of Sharepoint and its possibilities. From the most interesting I would distinguish such features as cross-site publishing, managed metadata navigation, friendly URLs. They moved Sharepoint on the next level, but at the same time introduced many new challenges to developers and IT professionals. In my blogs and forums answers I tried to help people to overcome these challenges and I’m glad that it was evaluated by MS.

Thanks MS for the award and thanks to community for commenting my posts, downloading my open source libraries and for other support I got for my work. I’m looking forward for new challenges, interesting IT quests and inspiring development journeys.

Saturday, March 29, 2014

Problem with SEO properties for pages with friendly URLs in Sharepoint 2013

Sharepoint 2013 has OTB SEO features which allow you to optimize your sites for search engines. In order to use them you need to activate SearchEngineOptimization feature. After that in ribbon on your publishing pages there will be “Edit SEO properties” command:

image

The problem is that if your friendly URL equal to the real site’s URLs (which is not bad because it is possible to switch to structural URLs and your links will still work, because http://example.com/test will be automatically resolved to http://example.com/test/pages/default.aspx), then “Edit SEO properties” command will be disabled:

image

However it is still possible to edit SEO properties using direct URL. E.g. if we are on http://example.com/test site. In order to edit SEO properties open the following URL in the browser:

http://example.com/test/_layouts/15/SEOProperties.aspx?Source=/test/pages/default.aspx&page=1

There are several important moments:

  1. URL should be relative to the current site (http://example.com/test), not e.g. for the root site of site collection;
  2. Source query string param should contain server relative real URL of the page (/test/pages/default.aspx).

If you made it correctly, you should see SEO properties page opened and after saving changes will be applied to the page, regardless of real or friendly URL are used in browser:

image

Hope that this workaround will help you.

Startup code in Orchard module

If you work with some CMS it will be better if you will play by its rules. E.g. in Sharepoint basic way to add customizations is feature. It allows you to add custom functionality to the site. In Orchard the basic way to apply customization is module. There may be a lot of custom modules in your project and the more each module will be independent from others the easier maintenance will be.

In the module you may also implement custom functionality like controllers, views and models. Some of this functionality may require adding of initialization code (like adding custom model binder), which in regular ASP.Net MVC application goes to global.asax.cs Application_Start() method. Orchard has own global.asax, but it is located in the common Orchard.Web project which is not related with your module. Is there a way to add initialization code which will be executed on startup for the module? Yes, and it is quite simple. In order to do it we need to add a class which inherits Autofac.Module class to the module’s project (Autofac is IoC library used in Orchard) and override its Load method:

   1: public class ContactFormModule : Module
   2: {
   3:     private bool isInitialized = false;
   4:     protected override void Load(ContainerBuilder builder)
   5:     {
   6:         if (!this.isInitialized)
   7:         {
   8:             ModelBinders.Binders.Add(typeof (FooModel), new FooModelBinder());
   9:             this.isInitialized = true;
  10:         }
  11:     }
  12: }

And that’s basically all. Orchard will automatically wire it up and call during application initialization.