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

    Implementing a Secure Media library in Sitecore

    Our internal clients wanted us to secure some of their resources stored in the media library so that only authenticated and authorized extranet users are able to view or download them. Our current setup has the following components, I'll simply focus on the 3rd and 4th points:
    Sharepoint Workflows on a Sharepoint 2007 Server. This serves as the working repository of the resource owners.Sitecore Sharepoint Integration Framework. This hooks up Sitecore with the designated document library in Sharepoint for all finalized files/documentsIntegration folder in the Media libraryLogin page to handle the authenticationCreate your login page. You have the option to create your sublayout for this (mostly to control the page contents etc.) or you can just quickly create one and put it in one of your folders in your solution. I normally use CodeFile directive in my pages as we found it easier to maintain this type of setup in the long run. There are plenty of samples of the login functionality …

    Web Forms For Marketers - Custom Unique ID Field Type

    I recently had a request from the business to implement a unique 8-digit (auto-incrementing) Reference number for one of their Forms. Doing a quick scan in the available Field types, I found that there wasn't any type available that fits my requirement.  I had two options to consider:

    Override the Save to DB action: by overriding the Execute method and replacing the AdaptedResultList with my own which queries the WFM DB and looks for the Max Reference value and returns the incremented value at that instance; this worked well but I quickly ran into an issue wherein other Save Actions (like Send Email) were still using the original AdaptedResultList and thus the experience was inconsistent. If I were to go this route, I would have to override every possible Save action which is not very good. So I concentrated with my next option:  Create my own Custom Field Type: I created my own type, did the necessary changes in the WFFM module settings like (add a new Field Type referencing my n…