Skip to content

Instantly share code, notes, and snippets.

@abjerner
Created December 23, 2014 14:42
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save abjerner/bdd89e0788d274ec5a33 to your computer and use it in GitHub Desktop.

By using my Skybrud.Umbraco.GridData package (which introduces a strongly typed model for the Grid), indexing the new Grid in Umbraco 7.2 is quite easy.

At Skybrud.dk we typically create a class named ExamineIndexer that takes care of the Examine related events. This class should be initalized during Umbraco startup like this:

using Umbraco.Core;

namespace FanoeTest {

    public class Startup : ApplicationEventHandler {

        private static ExamineIndexer _examineIndexer;

        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {
            // Register events for Examine
            _examineIndexer = new ExamineIndexer();
        }

    }

}

The ExamineIndexer class it self will now look like the example below. The example is very specific, since only certain document types where we know that the content property holds a Grid value are handled.

The example could be modified to use the Content Service for finding all properties that holds a Grid value. But since the Content Service uses the database, it may slow the performance when building your index.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Examine;
using Examine.Providers;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Values;
using Umbraco.Core.Logging;

namespace FanoeTest {
   
    public class ExamineIndexer {

        public ExamineIndexer() {

            BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

            externalIndexer.GatheringNodeData += OnExamineGatheringNodeData;

        }

        private void OnExamineGatheringNodeData(object sender, IndexingNodeDataEventArgs e) {

            try {

                string nodeTypeAlias = e.Fields["nodeTypeAlias"];

                LogHelper.Info<ExamineIndexer>("Gathering node data for node #" + e.NodeId + " (type: " + nodeTypeAlias + ")");

                if (nodeTypeAlias == "Home" || nodeTypeAlias == "LandingPage" || nodeTypeAlias == "TextPage" || nodeTypeAlias == "BlogPost") {

                    string value;

                    if (e.Fields.TryGetValue("content", out value)) {

                        LogHelper.Info<ExamineIndexer>("Node has \"content\" value\"");

                        GridDataModel grid = GridDataModel.Deserialize(e.Fields["content"]);

                        StringBuilder combined = new StringBuilder();

                        foreach (GridControl ctrl in grid.GetAllControls()) {

                            switch (ctrl.Editor.Alias) {

                                case "rte": {

                                    // Get the HTML value
                                    string html = ctrl.GetValue<GridControlRichTextValue>().Value;

                                    // Strip any HTML tags so we only have text
                                    string text = Regex.Replace(html, "<.*?>", "");

                                    // Extra decoding may be necessary
                                    text = HttpUtility.HtmlDecode(text);

                                    // Now append the text
                                    combined.AppendLine(text);
                                    
                                    break;
                                
                                }
                                
                                case "media": {
                                    GridControlMediaValue media = ctrl.GetValue<GridControlMediaValue>();
                                    combined.AppendLine(media.Caption);
                                    break;
                                }

                                case "headline":
                                case "quote": {
                                    combined.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;
                                }

                            }

                        }

                        e.Fields["content"] = combined.ToString();

                    } else {

                        LogHelper.Info<ExamineIndexer>("Node has no \"content\" value\"");

                    }

                }

            } catch (Exception ex) {

                LogHelper.Error<ExamineIndexer>("MAYDAY! MAYDAY! MAYDAY!", ex);
            
            }

        }

    }

}
@abjerner
Copy link
Author

The trick is to split the controls of the grid into different fields depending on the type of the control.

In the example below, values of rte and headline are added to the contentFocus field, while remaining values are added to the contentNormal field.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Examine;
using Examine.Providers;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Values;
using Umbraco.Core.Logging;

namespace FanoeTest {

    public class ExamineIndexer {

        public ExamineIndexer() {

            BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

            externalIndexer.GatheringNodeData += OnExamineGatheringNodeData;

        }

        private void OnExamineGatheringNodeData(object sender, IndexingNodeDataEventArgs e) {

            try {

                string nodeTypeAlias = e.Fields["nodeTypeAlias"];

                LogHelper.Info<ExamineIndexer>("Gathering node data for node #" + e.NodeId + " (type: " + nodeTypeAlias + ")");

                if (nodeTypeAlias == "Home" || nodeTypeAlias == "LandingPage" || nodeTypeAlias == "TextPage" || nodeTypeAlias == "BlogPost") {

                    string value;

                    if (e.Fields.TryGetValue("content", out value)) {

                        LogHelper.Info<ExamineIndexer>("Node has \"content\" value\"");

                        GridDataModel grid = GridDataModel.Deserialize(e.Fields["content"]);

                        StringBuilder contentFocus = new StringBuilder();
                        StringBuilder contentNormal = new StringBuilder();

                        foreach (GridControl ctrl in grid.GetAllControls()) {

                            switch (ctrl.Editor.Alias) {

                                case "rte": {

                                    // Get the HTML value
                                    string html = ctrl.GetValue<GridControlRichTextValue>().Value;

                                    // Strip any HTML tags so we only have text
                                    string text = Regex.Replace(html, "<.*?>", "");

                                    // Extra decoding may be necessary
                                    text = HttpUtility.HtmlDecode(text);

                                    // Now append the text
                                    contentFocus.AppendLine(text);

                                    break;

                                }

                                case "media": {
                                    GridControlMediaValue media = ctrl.GetValue<GridControlMediaValue>();
                                    contentNormal.AppendLine(media.Caption);
                                    break;
                                }

                                case "headline":
                                    contentFocus.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;

                                case "quote": {
                                    contentNormal.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;
                                }

                            }

                        }

                        e.Fields["contentFocus"] = contentFocus.ToString();
                        e.Fields["contentNormal"] = contentNormal.ToString();

                    } else {

                        LogHelper.Info<ExamineIndexer>("Node has no \"content\" value\"");

                    }

                }

            } catch (Exception ex) {

                LogHelper.Error<ExamineIndexer>("MAYDAY! MAYDAY! MAYDAY!", ex);

            }

        }

    }

}

Then your search query could then look something like contentFocus:"hest"^2 contentNormal:"hest". The contentFocus field will then have twice the importance of the contentNormal field.

That should do the job ;)

@theDiverDK
Copy link

Thanks Anders

Cool way to solve my problem :) I can see i need to dig deeper with Examine, to try to understand better how it is working :)

@AussieInSeattle
Copy link

Nice work!

Can you provide an example for extending the Property Converters you've built in - as an example I have a custom headline grid editor that has value.title and value.subhead values - I know I could access them via JObject, but would prefer the strongly typed model approach. One issue I've also seen with custom grid editors is that sometimes value.subhead will actually be null (subhead wont even exist) if the user never enters anything into that textarea, so not sure if your Value Converter approach handles that?

Thanks,
Matt

@abjerner
Copy link
Author

Hi Matt. Sorry for not answering sooner - I hadn't seen your comment.

From what I understand, the JSON for your value looks like:

{
    "title": "Here is my title",
    "subhead": "Here is my sub header."
}

Implementing a class for your value could then look like below. We map the individual properties using JSON.net.

using Newtonsoft.Json;
using Skybrud.Umbraco.GridData.Interfaces;

public class GridControlHeaderValue : IGridControlValue {

    [JsonProperty("title")]
    public string Title { get; set; }

    [JsonProperty("subhead")]
    public string SubHeader { get; set; }

}

Then you need to register your editor so that the Grid model will automatically parse your value. Assuming that the alias of your editor is header, that can be done by the following line during startup:

using Newtonsoft.Json.Linq;
using Skybrud.Umbraco.GridData;
using Umbraco.Core;

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        GridContext.Current["header"] = token => token.ToObject<GridControlHeaderValue>();

    }

}

If you don't like the lambda syntax, the snippet below does exactly the same:

using Newtonsoft.Json.Linq;
using Skybrud.Umbraco.GridData;
using Umbraco.Core;

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        GridContext.Current["header"] = delegate(JToken token) {
            return token.ToObject<GridControlHeaderValue>();
        };

    }

}

On the less serious side, if you like Easter eggs, this is also possible :bowtie:

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        PiggyBank.OneLittlePiggy["header"] = token => token.ToObject<GridControlHeaderValue>();

    }

}

I hope this answers your question. If not - and I'm to slow to answer - feel free to create an issue here on GitHub or ping me on Twitter 😏

@nvmy13t
Copy link

nvmy13t commented May 18, 2015

Anders,

I am trying to follow your example for creating a new property converter (similar to Matt's question above). When I try to register my editor in ApplicationStarted, I get an error "The name 'GridContext' does not exist in the current context." Can you provide any guidance?

Thanks,

Mike

@asaliyog
Copy link

Hi All,
I am trying to Index my custom grid editor data for search and following above example but stuck with an strange issue-
BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

above line of code throwing an exception -Additional information: Exception has been thrown by the target of an invocation ,"Exception has been thrown by the target of an invocation. (Y:\Projects\testProject\testProject.Web\config\ExamineSettings.config line 18)"}

and my examinSetting.config file is-

Any help will be appreciated

Thanks

@pbne04
Copy link

pbne04 commented Dec 2, 2015

Hello,

I am trying to index my grid data, but after updating to Umbraco 7.3.3 (from 7.2.2), I am getting some exceptions when using GridDataModel.Deserialize(e.Fields["myGrid"])

The error + stacktrace is as follows:

Object reference not set to an instance of an object.
   at Skybrud.Umbraco.GridData.Howdy.ReplaceEditorObjectFromConfig(GridControl control)
   at Skybrud.Umbraco.GridData.GridControl.Parse(GridArea area, JObject obj)
   at Skybrud.Umbraco.GridData.GridArea.<>c__DisplayClass4.<Parse>b__2(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridArea.Parse(GridRow row, JObject obj)
   at Skybrud.Umbraco.GridData.GridRow.<>c__DisplayClass14.<Parse>b__13(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridRow.Parse(GridSection section, JObject obj)
   at Skybrud.Umbraco.GridData.GridSection.<>c__DisplayClass2.<Parse>b__1(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridSection.Parse(GridDataModel model, JObject obj)
   at Skybrud.Umbraco.GridData.GridDataModel.<>c__DisplayClass2a.<Deserialize>b__29(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridDataModel.Deserialize(String json)

The griddata which produced this error is the following

"{\r\n  \"name\": \"1 column layout\",\r\n  \"sections\": [\r\n    {\r\n      \"grid\": 12,\r\n      \"rows\": [\r\n        {\r\n          \"name\": \"Headline\",\r\n          \"areas\": [\r\n            {\r\n              \"grid\": 12,\r\n              \"editors\": [\r\n                \"headline\"\r\n              ],\r\n              \"controls\": [\r\n                {\r\n                  \"value\": \"<p>test headline</p>\",\r\n                  \"editor\": {\r\n                    \"name\": \"Rich text editor\",\r\n                    \"alias\": \"rte\",\r\n                    \"view\": \"rte\",\r\n                    \"icon\": \"icon-article\"\r\n                  }\r\n                }\r\n              ]\r\n            }\r\n          ],\r\n          \"id\": \"782155a3-775e-640e-5998-de9f8d677cc6\"\r\n        },\r\n        {\r\n          \"name\": \"Content\",\r\n          \"areas\": [\r\n            {\r\n              \"grid\": 12,\r\n              \"allowAll\": true,\r\n              \"controls\":
 [\r\n                {\r\n                  \"value\": \"<p>test content</p>\",\r\n                  \"editor\": {\r\n                    \"name\": \"Rich text editor\",\r\n                    \"alias\": \"rte\",\r\n                    \"view\": \"rte\",\r\n                    \"icon\": \"icon-article\"\r\n                  }\r\n                }\r\n              ]\r\n            }\r\n          ],\r\n          \"id\": \"0d570fba-c971-499b-32c6-80cf6bbc8c6b\"\r\n        }\r\n      ]\r\n    }\r\n  ]\r\n}"

Do you have a suggestion as to what might have gone wrong here?

Regards Peter

EDIT:
I downloaded the source code, built the dll and used it in my project - works like a charm. I guess maybe the Nuget package is not quite up to date?

EDIT#2:
Looks like the package has now been updated, https://github.com/skybrud/Skybrud.Umbraco.GridData/releases/tag/v1.5.1

@darrenst
Copy link

darrenst commented May 6, 2016

I have manged to get this going. Thought I'd share my notes for newbee's as the instructions need a degree of modification for your own websites.

Create a new class that inherits from ApplicationEventHandler

protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { // Register events for Examine <NamespaceToClass>.ExamineIndexer _examineIndexer = new ExamineIndexer(); Log.Info("Examine SkyBrid Indexer is starting"); }

This will run a class ExamineIndexer() on startup.

Next Create your ExamineIndexer() class

Copy the example above but amend to account for your property names.

In the ExamineIndexer Class the example uses the externalIndexer but amend if you are rolling your own as I am (MySearchIndexer).

BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["MySearchIndexer"];

In the OnExamineGatheringNodeData method only include the Document Type ALIAS names you want to search

if (nodeTypeAlias == "Landing" || nodeTypeAlias == "StandardPage")

You only want to include document types that actually have the grid property included

Next amend the if statement to include the name of the grid property alias. Mine is called "BodyGrid"

if (e.Fields.TryGetValue("bodyGrid", out value))

is also here on the deserialize method

GridDataModel grid = GridDataModel.Deserialize(e.Fields["bodyGrid"]);

next the method is looking for Grid editor alias matching. If you have changed or added new editors you will need to include in the case statement else it won't be included in the index. The additions i made were:

                            case "paragraph":
                            case "headline_centered":

These values are rolled up into the existing field "bodyGrid"

e.Fields["bodyGrid"] = combined.ToString();

OK that's it Build and test

NOTES:

Ensure your custom indexer is actually indexing your customer property (bodyGrid)

In your Search View ensure your search criteria is selecting your grid property. This is the code that handles my search page:

var Searcher = Examine.ExamineManager.Instance.SearchProviderCollection["MySearchSearcher"];

var SearchCriteria = Searcher.CreateSearchCriteria(Examine.SearchCriteria.BooleanOperation.Or);

var Query = SearchCriteria.Field("heroTitle", searchTerm.Boost(3)) .Or() .Field("title", searchTerm.Fuzzy().Value.Boost(2)) .Or() .Field("subtitle", searchTerm.Fuzzy().Value.Boost(1)) .Or() .Field("bodyGrid", searchTerm.Fuzzy()) ;

That should do it.

@naepalm
Copy link

naepalm commented Dec 2, 2016

Just want to say I used this in 7.5.4 and it works like a charm. Thanks so much @abjerner :)

@slseverance
Copy link

I'm having an issue with the basic installation. I read the instruction on the grid.skybrud.dk site that says simply drop the dlls in the bin folder. So I have downloaded the ZIP and pulled the xmls and dlls out of the latest release folder and dropped them into the bin folder. I restarted the app pool. It's not working so I assume the installation is more complicated than that? I have built a template using your example code and my aliases so i feel confident I have simply not installed it correctly. Must this be built and recompiled before operation or will a simple restart of service pick up the new packages? I don't have a viz studio solution set up. Thank you.

@zipswich
Copy link

Thank you for the example. I have largely copied the code, and made sure ExamineIndexer() is called. Unfortunately, OnExamineGatheringNodeData is never invoked. Could you provide a tip on how to debug this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment