Your PC can't even




MVC - MVC Search In Model

  • Previously, I covered how to implement the methods for SearchInModel<T>. That blog post is listed here. This post shows how you can use those methods to implement a global search component. This example will be modifying a table collection that was setup with datatables.js based upon the search results. The results however, could be combined with other useful mechanics; not just with datatables.

  • First, we will cover the Search Global component.


GlobalSearchModel.cs for the View and Action
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace Models {
            public class GlobalSearchModel {
            public string EndpointController { get; set; }
            public string DataContext { get; set; }
		[Required(ErrorMessage = "Required Field")]
		[Display(Name = "Any Value")]
            public string Fragment { get; set; }
            public GlobalSearchModel() {
		}
	}
}
SearchGlobal.cshtml, this page does not specify a layout as it is generally rendered as a subcomponent. The resources it uses are as follows.
  • bootstrap.css and bootstrap.js
  • jquery and jquery validate
  • animate.css
  • The following custom css:
    basicDuration {
                                -o-animation-duration: 2s;
                                -moz-animation-duration: 2s;
                                -webkit-animation-duration: 2s;
                                animation-duration: 2s;
    }
    .accordion-element {
                                -webkit-transition:max-height 1s ease;
                                -moz-transition: max-height 1s ease;
                                -o-transition: max-height 1s ease;
                                transition: max-height 1s ease;
                                /*webkit/transition/ease only works with PX specs and not percentages*/
                                overflow:hidden;
                                max-height:0;
    }
    .apply-accordion {
                                /*Just needs to be a height greater than your element*/
                                max-height:500px;
    }

    @model Models.GlobalSearchModel
    @{
    @*Format this string to prevent oddities from the '/' char*@
    string IdFragment = string.Format("{0}", Model.DataContext.Replace(@"/", "-"));
    @*This will never provide a layout, this sub page is rendered on every tab for a fuzzy search*@
    Layout = null;
    }
            <script type="text/javascript">
        @{ Write(string.Format(< span class="hljs-string">"//# sourceURL=DynamicGlobalSearch-For-{0}.js"
    , IdFragment));}
    $(document).ready(function () {
            //Wire up a main for this guy that will render on callback from the main splash page
            console.log('Global Search Rendering: ' + "@Model.DataContext");
            var Main = function () {
            var PureFragment = "@IdFragment";
            var LocalContextRows = undefined;
            //Multi-facet, should attempt to be aware of a data context for tables.
            //If found in a context, allow a clickable link to filter and trigger that context appearance
            //Just because we have found results doesn't mean we need to navigate to them directly, let the user have control over that.
            var IdFragment = "@string.Format("Search-Form-{0}", IdFragment)";
            //This let's us know where we are operating with respect to. Further down this context will relate to the individual items it has found.
            var DataContext = "@Model.DataContext";
    @*Function scoped form*@
            var form = $('#' + IdFragment);
    @*Helper to serialize a form into an object literal*@
    $.fn.serializeObject = function () {
            var obj = {};

    $.each(this.serializeArray(), function (i, o) {
            var n = o.name,
    v = o.value;

    obj[n] = obj[n] === undefined ? v
    : $.isArray(obj[n]) ? obj[n].concat(v)
    : [obj[n], v];
    });

            return obj;
    };
            //Set the rules for validator
    $.validator.setDefaults({
            showErrors: function (errorMap, errorList) {
            // Clean up any tooltips for valid elements
    $.each(this.validElements(), function (index, element) {
            //Using Jquery tooltips so use Attr, if bootstrap uses data
            var $element = $(element);
    $element.tooltip().tooltip("destroy").tooltip().attr("title", "") // Clear the title - there is no error associated anymore
    .parent().removeClass("has-error").addClass("has-success").addClass("has-feedback");
            //Now Flip the glyphicon
    $element.siblings("span").addClass("glyphicon").removeClass("glyphicon-remove").addClass("glyphicon-ok");
    });
            // Create new tooltips for invalid elements
    $.each(errorList, function (index, error) {
            var $element = $(error.element);
    $element.tooltip().tooltip("destroy").tooltip().attr("title", error.message).tooltip("option", "position", { my: "center bottom", at: "center top-10" })
    .parent().removeClass("has-success").addClass("has-error").addClass("has-feedback");
            //Add Error glyphicon
    $element.siblings("span").addClass("glyphicon").removeClass("glyphicon-ok").addClass("glyphicon-remove");
    });
            this.defaultShowErrors();
    },
            ignore: ":hidden"
    });
            //Remove the validation rules applied by the libraries
    form.removeData('validator');
    form.removeData('unobtrusiveValidation');
            /// Parses all the HTML elements in the specified selector. It looks for input elements decorated
            /// with the [data-val=true] attribute value and enables validation according to the data-val-*
            /// attribute values.
            /// </summary>
            ///Any valid jQuery selector.
    $.validator.unobtrusive.parse(form);
    form.on('submit', function (e) {
            //stop the default action of a form, which is to post to some endpoint.
    e.preventDefault();
            var $form = $(this);
    $form.validate();
            console.log($(this).valid());
            if ($(this).valid() === true) {
            //Clear the previous section results
    $("#Results-for-" + PureFragment).removeClass('apply-accordion');
    $form.find('button').prop('disabled', true);
    $form.find('img').show();
    $.ajax({
            type: "POST",
            dataType: "json",
            data: $form.serializeObject(),
            xhrFields: {
            withCredentials: true //sends the auth cookie, if we have one
    },
            crossDomain: true,
            //You can change this Url to another Domain, this will act like the live site in terms of Not-Same-Origin policy
    url: "@Url.EnvironmentAction("SearchGlobal", Model.EndpointController)",
            success: function (data, textStatus, jqXHR) {
            console.log(data);
            ////Draw will calc our rows and add our button elements/imgages back into the view. We do not need to worry about flipping the props
    $form.find('button').prop('disabled', false);
    $form.find('img').hide();
            if (data !== undefined && data.ContextToRows !== undefined) {
            //Clear the previous section results
    $("#Results-for-" + PureFragment).html('');
            var HtmlSlots = [];
            console.log(data.ContextToRows);
    $.each(data.ContextToRows, function (key, value) {
            var _key = key;
            if (key.indexOf('|') !== -1) {
            //Split on | will be in the format Name|Value
            var splitElements = key.split('|');
            if (splitElements.length == 2) {
            //only works with exactly two element otherwise let the default work
    _key = splitElements[0];
            //_context = splitElements[1];
    }
    }
            if (value.length !== 0) {
            //The Key will give us access into our table, we need to modify the html for display and create a value collection
    HtmlSlots.push('<br/><div><strong>' + value.length + '</strong> entries found in <strong>' + _key + '</strong>: <a href="javascript:void(0);" data-context="' + key + '" class="btn btn-default btn-xs">click to display</a></div>');
    }
            else {
    HtmlSlots.push('<br/><div><strong>' + value.length + '</strong> entries found in <strong>' + _key + '</strong>:</div>');
    }
    });
    $("#Results-for-" + PureFragment).html(HtmlSlots);
    $("#Results-for-" + PureFragment).toggleClass('apply-accordion');
            //Store the global for access upon click event
    LocalContextRows = data.ContextToRows;
            //Wire-up the click events
    $($("#Results-for-" + PureFragment + ' a')).on('click', function () {
            var element = $(this);
            //This is the key to everything. We need to load the table, show the tab, trigger the event.
            //The context let's us know where we are operating from. Makes this reusable for multiple tabs.
            var elementContext = element.data('context');
            var rowContext = element.data('context');
            if (elementContext.indexOf('|') !== -1) {
            //Split on | will be in the format Name|Value
            var splitElements = elementContext.split('|');
            if (splitElements.length == 2) {
            //only works with exactly two element otherwise let the default work
    elementContext = splitElements[1];
    }
    }
            //Rows to load, based upon the data context.
            var elementRows = LocalContextRows[rowContext];
            //Target Table, also based upon the data context.
            var ContextTable = window.TableCollection[elementContext];
            console.log(elementRows);
    ContextTable.clear();
    ContextTable.rows.add(elementRows);
    ContextTable.draw();
            //Show the tab.
    $($('.nav-link[data-context="' + elementContext + '"]')).tab('show');
            //Trigger the event for the context, if we have one.
    $(document).trigger(elementContext);
    });
    }

    },
            error: function () {
            console.log("error searching context:" + DataContext);
    $form.find('button').prop('disabled', false);
    $form.find('img').hide();
    }
    });
    }
    });
    };
    Main();
    });
            </script>
            <br />
            <div class="alert alert-info">
            <p><strong>Global Search: </strong>Click 
        <a clickignore="true" href="javascript:void(0);" onclick=
    "$('@string.Format("
#Search-Form-{0}",< span class="hljs-attr">IdFragment)').toggleClass('apply-accordion');" id="@string.Format("Search-ref-{0}",IdFragment)" class="btn btn-default btn-xs">here</a> to search across all tabs.</p>
            
                <form class="accordion-element" id="@string.Format("Search-Form-{0}",IdFragment)">
            <div class="form-group">
            <div>
                @Html.LabelFor(model => model.Fragment, new { @class = "control-label" })
                @Html.EditorFor(model => model.Fragment, new { htmlAttributes = new { @class = "form-control" } })
            <span class="glyphicon form-control-feedback" aria-hidden="true"></span>
                @Html.HiddenFor(Model = > Model.DataContext)
            </div>
            <br />
            <div>
            <button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-search"></i> Search</button>
            
                    <img style="display:none" src="@Url.EnvironmentContent("~/Content/images/loading.gif")" />
            </div>
            </div>
            
                        <section id="@string.Format("Results-for-{0}",< span class= "hljs-attr" > IdFragment )" class="basicDuration alert-success accordion-element"></section>
            </form>
            </div>
Controller.cs NOTE: this example will be based off a system that takes feature requests from users.
namespace Controllers
{
            //Extended Controller is not relevant for this example, but will be covered in the future
            public class FeatureRequestAdminController : ExtendedController
    {
            private List<Dictionary<string, object>> SearchForDataChunk(string Id, string Fragment, out HashSet<Type> FoundIn) {
            FoundIn = new HashSet<Type>();
            List<FeatureRequest> SelectedChunks = new List<FeatureRequest>();
            using (WebDbDataContext context = new WebDbDataContext(Constants.WebConnectionString)) {
            var options = new System.Data.Linq.DataLoadOptions();
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestProduct);
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestSeverity);
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestOperatingSystem);
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestVersion);
                context.LoadOptions = options;
            base.SearchInModel(Fragment, context.FeatureRequests.Where(x => x.FR_ProductId == new Guid(Id) && x.Approved == null).OrderBy(x => x.CreatedDate).ToList(), d => d.Id, out FoundIn, out SelectedChunks);
            }

            return this.NormalizeGridData(SelectedChunks);
        }
            private List<Dictionary<string, object>> NormalizeGridData(List<FeatureRequest> Chunk, bool highlight = false) {
            //The NameValueCollection will contain our chunked data that we will return as json back to the calling ajax
            List<Dictionary<string, object>> nvc = new List<Dictionary<string, object>>();
            Func<object, string, string> PutInDiv = (o, g) => {
            if (g != null) {
            return string.Format(@"<div {0}>{1}</div>", g, o);
                }
            else {
            return string.Format(@"<div>{0}</div>", o);
                }
            };
            Chunk.ToList().ForEach(x => {
                Dictionary<string, object> _nvc = new Dictionary<string, object>();
            //DT_RowId is the actual id of the row, Datatables.js will add it automagically
                _nvc.Add("DT_RowId", x.Id);
            //Same but With row class
            if (highlight) {
                    _nvc.Add("DT_RowClass", "highlightnew");
                }
                _nvc.Add("Actions", PutInDiv(string.Format(@"<a data-action=""Details"" a-role=""action"" class=""btn btn-info btn-sm"" href=""javascript:void(0);""><i class=""glyphicon glyphicon-comment""></i> Show Description</a><a style=""margin-left:5px;"" data-action=""Approve"" a-role=""action"" class=""btn btn-success btn-sm"" href=""javascript:void(0);""><i class=""glyphicon glyphicon-ok-sign""></i> Approve</a><a style=""margin-left:5px;"" data-action=""Send"" a-role=""action"" class=""btn btn-danger btn-sm"" href=""javascript:void(0);""><i class=""glyphicon glyphicon-remove-sign""></i> Deny</a>"), string.Format(@"data-house=""actions""")));
            //<th>Actions</th>
            //<th>Description</th>
            //<th>Version</th>
            //<th>OS</th>
            //<th>First Name</th>
            //<th>Last Name</th>
            //<th>Email</th>
            //<th>Created Date</th>
                _nvc.Add("Summary", PutInDiv(x.Summary, null));
                _nvc.Add("Version", PutInDiv(x.FeatureRequestVersion.Version, null));
                _nvc.Add("OS", PutInDiv(x.FeatureRequestOperatingSystem.OperatingSystem, null));
                _nvc.Add("First Name", PutInDiv(x.FirstName, null));
                _nvc.Add("Last Name", PutInDiv(x.LastName, null));
                _nvc.Add("Email", PutInDiv(x.Email, null));
                _nvc.Add("Created Date", PutInDiv(x.CreatedDate, null));


            foreach (var entry in _nvc.Keys.ToList()) {
            try {
                        _nvc[entry] = _nvc[entry].ToString().Replace("\r", "").Replace("\n", "");
                    }
            catch { }
                }
                nvc.Add(_nvc);
            });
            return nvc;
        }
		
	[HttpPost]
            public ActionResult SearchGlobal(GlobalSearchModel model) {
            HashSet<Type> FoundIn = new HashSet<Type>();
            Dictionary<string, List<Dictionary<string, object>>> ContextToRows = new Dictionary<string, List<Dictionary<string, object>>>();
            List<FeatureRequestProduct> SelectedChunks = new List<FeatureRequestProduct>();
            using (WebDbDataContext context = new WebDbDataContext(Constants.WebConnectionString)) {
            var options = new System.Data.Linq.DataLoadOptions();
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestProduct);
                options.LoadWith<FeatureRequest>(x => x.FeatureRequestSeverity);
                context.LoadOptions = options;
                List<string> UniqueAvailablenames = context.FeatureRequests.Where(x => x.Approved == null).Select(x => x.FeatureRequestProduct.ProductName).Distinct().ToList();
            //Now gather the products
                SelectedChunks = context.FeatureRequestProducts.Where(x => UniqueAvailablenames.Contains(x.ProductName)).ToList();
            }
            foreach (var entry in SelectedChunks) {
            //This is special compared to just adding the name as the context (which the script handles by default).
            //This allows us to map the display name and logic separately as other tabs in our site have consumed some of the keys.
                ContextToRows.Add(string.Format("{0}|{1}",entry.ProductName,entry.Id.ToString()), SearchForDataChunk(entry.Id.ToString(),model.Fragment.Trim(), out FoundIn));
            }
            return new JsonResult() { Data = new { ContextToRows = ContextToRows }, JsonRequestBehavior = JsonRequestBehavior.DenyGet, MaxJsonLength = int.MaxValue };
        }
	}
}
Calling the component in your view.
Html.RenderPartial("~/Views/Shared/SearchGlobal.cshtml", new Models.GlobalSearchModel() { DataContext = "FeatureRequest", EndpointController = "FeatureRequestAdmin" });
Here is how it looks rendered in its various states.