Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Incremental Search on the Web with Dynamic LINQ + SignalR

5.00/5 (7 votes)
15 Jul 2017Apache4 min read 22.2K  
Application of MVVM over SignalR library described in a previous CodeProject tip, combined with Dynamic LINQ library to do incremental web search by typing the query

Introduction

The web-based defect tracking system I've been using is great for the most part, but if there's something that could be improved, it's the visual query builder that creates search filters. When I need to find certain defects on category X, submitted by either A or B during the month of July - or was it May? - and not sure whether it's been closed or merely ready for test, then what follows is a flurry of clicks on nested menu that pops one after the other to build the desired query expression.

Being a software developer, I could have typed the query myself and be done with it a lot quicker, but unfortunately that free-form typing feature is not offered. So I started to ask myself, how would I implement a list search system - on the web - that allows a free-form query typing with a bit of auto-complete helper, and to make it more interesting, produce results incrementally as the query is being typed?

The good news is, there is already a Microsoft library, System.Linq.Dynamic, that's capable of parsing a query string into a LINQ expression tree. I can just build a web application around this functionality.

In the previous CodeProject tip, "Real-time Web Made Simple with MVVM Pattern Over SignalR", I wrote about my open-sourced library named dotNetify which makes it very simple to do web apps as it does away with writing the entire RESTful service layer, and replacing it with automated, declarative 2-way MVVM binding. This would be a perfect job for it.

Implementation

With dotNetify, the principal source code becomes very simple, consisting of only an HTML view and a .NET view model. Even though this runs on the web, I don't need to write any JavaScript for this.

Let's look at the view model first.

View Model

C#
public class AFITop100VM : BaseVM
   {
      public List<MovieRecord> Movies
      {
         get
         {
            IEnumerable<MovieRecord> results;

            if (!String.IsNullOrEmpty(Query))
               results = AFITop100Model.AllRecords.Where(Query);
            else
               results = AFITop100Model.AllRecords;

            return Paginate(results);
         }
      }

      public string Query
      {
         get { return Get<string>(); }
         set
         {
            if (EnableAutoComplete)
               AutoComplete(ref value);

            Set(value);

            if (IsQueryValid(value))
               Changed(() => Movies);
         }
      }

      public string QueryError
      {
         get { return Get<string>(); }
         set { Set(value); }
      }

      //...Properties/fields associated with auto-complete and pagination removed for brevity...

      private List<MovieRecord> _queryTest = new List<MovieRecord>();
      private int _errorPos;

      // Returns whether a query expression is valid.
      // If not, it will set the QueryError property.
      private bool IsQueryValid(string iQuery)
      {
         QueryError = String.Empty;
         if (String.IsNullOrEmpty(iQuery))
            return true;

         try
         {
            _queryTest.Where(iQuery);
            return true;
         }
         catch (ParseException ex)
         {
            QueryError = ex.Message;
            _errorPos = ex.Position;
            return false;
         }
      }

      // Paginates the query results.
      private List<MovieRecord> Paginate(IEnumerable<MovieRecord> iQueryResults)
      {
         //...removed for brevity...
      }

      // Auto-completes a query expression in response to user typing the first letter of
      // a known model property or a LINQ method at the end of the expression.
      private void AutoComplete(ref string iQuery)
      {
         //...removed for brevity...
      }
   }

This is an abstraction of the view. It inherits from BaseVM class from the dotNetify library, which I discussed in the mentioned CodeProject tip. It essentially abstracts out the communication nitty gritty between the view model on the server-side and the view on the client-side as an MVVM pattern. When you set a value to the view model property, you can be sure that the value will be sent out to the bound view element on the browser, and the input made on the browser gets sent back and updated to the view model property.

There is the Query property that is bound to the text box to capture your query string as you type, and also send out the auto-completed keyword back to the browser, courtesy of 2-way binding.

As the query gets typed, it gets evaluated in real-time by the System.Linq.Dynamic library in the IsQueryValid method, and either it throws an exception whose error message gets sent to the browser through QueryError property, or the process flow continues to raising the changed event on the Movies property.

The Movies property accessor applies the query string on the Movies list (our model) and returns the filtered list to the browser.

The results are paginated and there are other view model properties that deal with pagination links, but the details for this and the crude auto-complete function I consider out of scope for this CodeProject tip.

View

HTML
<html>
<head>
   <link href="/Content/bootstrap.min.css" rel="stylesheet">
   <script src="/Scripts/require.js" data-main="/Scripts/app"></script>
</head>
<body class="container">
   <h2>AFI's 100 Greatest American Films of All Time</h2>
   <div data-vm="AFITop100VM">
      <div class="well">

         <!-- Query input box -->
         <div class="input-group">
            <span class="input-group-addon">Search:</span>
            <input class="form-control" 
            type="text" data-bind="textInput: Query" />
         </div>

         <!-- Parse exception error box -->
         <div class="label label-danger pull-right" 
         data-bind="text: QueryError"></div>
      </div>

      <!-- Movies list -->
      <table class="table table-hover table-striped panel-primary">
         <thead>
            <tr>
               <th>Rank</th>
               <th>Movie</th>
               <th>Year</th>
               <th>Cast</th>
               <th>Director</th>
            </tr>
         </thead>
         <tbody data-bind="foreach: Movies">
            <tr>
               <td><div data-bind="text: Rank" /></td>
               <td><div data-bind="text: Movie" /></td>
               <td><div data-bind="text: Year" /></td>
               <td><div data-bind="text: Cast" /></td>
               <td><div data-bind="text: Director" /></td>
            </tr>
         </tbody>
      </table>

      <!-- ...Pagination link markups removed for brevity -->
   </div>
</body>
</html>

DotNetify client-side library, loaded through the module loader Require.js, uses the data-vm attribute to identify the view model scope. When this HTML is loaded on the browser, it will automatically make a server request to fetch initial data from the named view model.

The data flows between the server view model and the HTML view through the binding mechanism that are defined declaratively on the element markups. The data-bind binding notations belong to Knockout.js library, which not only provide binding with input elements (i.e. textInput, text), but can also generate elements dynamically, e.g., the foreach binding which generates the table row elements for each list item in the Movies property it's bound to.

And that's it! With dotNetify, business logic stays on the server-side while the client-side only handles the UI, and without manually writing the logic to make the communication happen.

Live Demo

Live demo is available on my website at http://dotnetify.net/index/AFITop100.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0