Skip to main content

Add Export to File Functionality in Sitecore's Search Options

One of our business users was requesting for a listing of items they've already added and have it available in a format that can be opened in a spreadsheet (Excel). My initial thought was to create a blank aspx page and write all the logic to get the data in the code-behind, run it and save the resulting file to a csv then I'm done.
But then it got me to think, it might be a better idea to have this functionality plugged in to Sitecore and made available for everyone to use. 
Sitecore Bucket's Search Options fly-out seemed a good candidate for this feature (See image below). So doing some quick readings I got myself in to the "zone" and started implementing this quick and dirty PoC.


Just to explain what it actually does.

User will basically do a search, (Note that all search options require some filters or search keyword before any of the options can be used) clicks the Export to File, it pops up a dialog to confirm the action and executes, after which a browser-download dialog appears for the user to save the file to his/her local (in this case a CSV file).

Getting down to the details, here's a rundown of what I did:
  1. Go to Settings > Buckets > Settings > Search Operations : you will find here all of the search options available; grouped by their own folders. I created my own called User Defined and added the Export To File item (based from Dropdown List template). Put in the Type (more on this later) e.g. bucket:exporttofile. Just by adding this item, this will automatically get added to the Search Option fly out; it does not do anything yet at this point obviously.
  2. Now, let's hook it up with a processor. To do that, we would need to modify the following file: App_Config/Include/Sitecore.Buckets.config, find the commands section and add your command entry there then Save e.g. 
     
  3. Create your custom processor. To do that you would need to create a class that inherits from Sitecore.Shell.Frameworks.Commands.Command class and implement the Execute method. This solution uses SC's SheerResponse and ProgressBox as well as the Filestream object for the file handling. Here's the code:
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Web;
    using Sitecore;
    using Sitecore.Buckets.Util;
    using Sitecore.ContentSearch;
    using Sitecore.ContentSearch.SearchTypes;
    using Sitecore.ContentSearch.Utilities;
    using Sitecore.Data.Items;
    using Sitecore.Diagnostics;
    using Sitecore.Globalization;
    using Sitecore.Shell.Applications.Dialogs.ProgressBoxes;
    using Sitecore.Shell.Framework.Commands;
    using Sitecore.Text;
    using Sitecore.Web.UI.Sheer;
    
    namespace YourNamespace.Commands
    {
        [Serializable]
        public class ExportSearchToFile : Command
        {
    
            private string _linkToFile = string.Empty;
            // Methods
            public override void Execute(CommandContext context)
            {
                if (context.Items.Length <= 0) return;
                var item = context.Items[0];
                var parameters = new NameValueCollection();
                parameters["id"] = item.ID.ToString();
                parameters["language"] = item.Language.ToString();
                parameters["version"] = item.Version.ToString();
                parameters["database"] = item.Database.Name;
                var strings = context.Parameters.GetValues("url");
                if (strings != null)
                    parameters["searchString"] = strings[0].Replace("\"", string.Empty);
                Context.ClientPage.Start(this, "Run", parameters);
            }
    
            protected void Run(ClientPipelineArgs args)
            {
                Assert.ArgumentNotNull(args, "args");
                if (args.IsPostBack)
                {
                    if (args.HasResult)
                    {
                        var database = Sitecore.Configuration.Factory.GetDatabase(args.Parameters["database"]);
                        Assert.IsNotNull(database, "Database \"" + args.Parameters["database"] + "\" not found.");
                        var item =
                            database.Items[args.Parameters["id"], Language.Parse(args.Parameters["language"]),
                                Sitecore.Data.Version.Parse(args.Parameters["version"])];
                        var list = UIFilterHelpers.ExtractSearchQuery(args.Parameters["searchString"]);
                        Assert.IsNotNull(item, "item");
                        var jobName = Translate.Text("Export to File");
                        var title = Translate.Text("Exporting list to file");
                        const string icon = "~/icon/Software/32x32/text_code.png";
                        var parameters = new object[] {item, list, args.Result};
                        ProgressBox.Execute(jobName, title, icon, StartProcess, parameters);
                        SheerResponse.Download(_linkToFile);
                            //Alert(string.Format("Finished exporting all search results. Please click this link to download file: {0}", _linkToFile ));
                    }
                }
                else
                {
                    //create your own XAML -- I reused LayoutDetails for 
                    var str4 = new UrlString(UIUtil.GetUri("control:LayoutDetails"));
                    str4.Append("id", args.Parameters["id"]);
                    str4.Append("la", args.Parameters["language"]);
                    str4.Append("vs", args.Parameters["version"]);
                    SheerResponse.ShowModalDialog(str4.ToString(), true);
                    args.WaitForPostBack();
                }
            }
    
            private void StartProcess(params object[] parameters)
            {
                var str = (string) parameters[2];
                var item = (Item) parameters[0];
                SitecoreIndexableItem indexable = item;
                if (indexable == null)
                {
                    Log.Error("Export to file - Unable to cast current item - " + parameters[0].GetType().FullName, this);
                }
                else
                {
                    var searchStringModel = (List<SearchStringModel>) parameters[1];
                    using (
                        var context =
                            ContentSearchManager.GetIndex(indexable)
                                                .CreateSearchContext())
                    {
                        var count = 0;
                        IQueryable<SitecoreUISearchResultItem> queryable = LinqHelper.CreateQuery(context, searchStringModel, indexable);
                        //var list2 = queryable.Select(item3 => item3.GetItem()).Where(item4 => (item4 != null) && item4.Security.CanWrite(Context.User)).ToList();
                        var list2 = new List();
                        foreach (SitecoreUISearchResultItem item3 in queryable)
                        {
                            var item4 = item3.GetItem();
                            if (item4.Security.CanWrite(Context.User))
                            {
                                list2.Add(item4);
                            }
                        }
                        if (list2.Count > 0)
                        {
                            StreamWriter writer = null;
                            try
                            {
                                var filename = DateTime.Now.ToString("g") + "_Export";
                                var path = "/temp/" + filename + ".csv";
                                var path2 = path.Replace("/", "\\").Substring(1, path.Length-1);
                                var fs = new FileStream(HttpRuntime.AppDomainAppPath + path2, FileMode.Create, FileAccess.Write);
                                writer = new StreamWriter(fs, Encoding.Default);
                                writer.WriteLine("ID,Name,TemplateName,Parent,Version,Last-Modified");
                                
                                foreach (var item5 in list2)
                                {
                                    count++;
                                    if (item5 != null)
                                        writer.WriteLine(string.Format("{0},{1},{2},{3},{4},{5}",
                                                                   item5.ID, item5.Name, item5.TemplateName,
                                                                   item5.Parent.Name, item5.Version.Number,
                                                                   item5.Statistics.Updated.ToString(
                                                                       CultureInfo.InvariantCulture)));
                                }
    
                                writer.Flush();
                                writer.Close();
    
                                Log.Audit(this, "Export to file details: Exported a total of {0} items",
                                          new string[] {count.ToString(CultureInfo.InvariantCulture)});
    
                                _linkToFile = path; // @"";
                            }
                            catch (Exception ex)
                            {
                                Log.Error("Exception in Export to File - Search: " + ex.Message, this);
                                _linkToFile = @"An error occurred - please contact Support.";
                                if (writer != null)
                                    writer.Close();
                            }
                        }
                    }
                }
            }
        }
    }
     
    
    



  • Compile it and make sure your Command type matches with what you put in the config. Then try it out. Happy coding :)













  • Comments

    1. Hi,

      Need your help.

      I am not able to declare List type as u have done in above code as
      var searchStringModel = (List) parameters[1];

      I am getting type mismatch error for variable searchStringModel
      Can you please suggest solution to it.

      Thanks in advance

      Best Regards
      Anuj Chawla

      ReplyDelete
      Replies
      1. hi Anuj,

        The code did not post correctly. I have updated the code. Kindly check it out. It should have had this as it's List type:SearchStringModel

        Thanks

        Delete
    2. Hi, I get
      System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. ---> System.NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt. bei Sitecore.Web.UI.Sheer.ClientPage..ctor() bei Sitecore.Web.UI.Sheer.SheerResponse.Download(String file) bei Dornbracht.Backend.Commands.ExportSalesPartner.StartProcess(Object[] parameters)

      when I reach SheerResponse.Download(_linkToFile); Can you help me?

      ReplyDelete
    3. Could you check your actual link string in debug to see if it's in the proper format? Also, check if you have the /temp folder which I used in the example. Regards

      ReplyDelete
    4. Thanks for the blog Dean, I think it will help me quite a bit, unfortunately I'm having a bit of trouble at compile.. I'm getting a

      Using the generic type 'System.Collections.Generic.List' requires 1 type arguments

      at the line of code var list2 = new List();

      ReplyDelete
    5. There appears to be a step or 2 missing. Aside from the error I posted about yesterday, when I do get it somewhat working and I try the button it brings up the layout dialogue.

      ReplyDelete
      Replies
      1. Hi Shane,
        I had the same errors as you, but got it working when i did the following:
        List() has to be replaced by List()
        var filename = DateTime.Now.ToString("g") + "_Export";
        has to be replaced by
        var filename = DateTime.Now.Ticks.ToString() + "_Export"; (the file name had illegal characters)
        if you use sitecore 8 you also need to replace
        UIFilterHelpers.ExtractSearchQuery
        with Sitecore.ContentSearch.Utilities.SearchStringModel.ExtractSearchQuery

        Apparently you can ignore the layout-box (in the code it says that you should put your own xaml here) - just press the OK-button and the file gets generated and prepared for download (at least on my pc)

        Delete

    Post a Comment

    Popular posts from this blog

    Add/Allow a different media URL prefix in Sitecore (aside from tilde ~) in Sitecore 7.0

    We recently had a requirement when one of our external vendors required that our media URLs should not include the "~" in the link as their system could not process those correctly. I found a few articles in the web, but most of them would suggest changing the default behavior, i.e. any "new" media item would have the 'new' replacement character though still supporting the tilde "~". Based on the web articles, I started with the following config keys and section: Media.RequestExtension Media.MediaLinkPrefix customHandlers We did not want to change any of the default behaviors, we just needed a way to make Sitecore support the additional URL media prefix. While sifting through the configuration, I chanced upon the following section: <mediaPrefixes>  According to the comment above it: Allows you to configure additional media prefixes (in addition to the prefix defined by the Media.MediaLinkPrefix setting)            The

    Brightcove Index Configuration for Coveo for Sitecore

    We recently moved our search provider from using Endeca and Lucene to Coveo for Sitecore. One of the areas that broke was the Brightcove index. You may follow the sample below to be able to get your Brightcove index working again:  1) In your Brightcove contentsearch config, comment out or remove the sections highlighted below 2) Replace this with the one below: 3) Check the comments in the image above to determine what to use for your DefaultIndexConfiguration 4) Comment or remove the entire section/block 5) Your final config may look something like below Hope this helps someone. Thanks for reading.