Click here to Skip to main content
14,218,892 members

Incremental Search on the Web with Dynamic LINQ + SignalR

Rate this:
5.00 (7 votes)
Please Sign up or sign in to vote.
5.00 (7 votes)
15 Jul 2017Apache
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

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>
<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

Share

About the Author

dsuryd
United States United States
Full-stack s/w engineer, open source author.

Comments and Discussions

 
QuestionSome comments... Pin
Dewey4-Jan-16 20:41
memberDewey4-Jan-16 20:41 
IMO, developers, unlike end users, don't like "Hidden". One of your goals in this framework is to hide javascript, and another is to hide data transfer to some extent.

As a developer, I like to have the option to send and receive data and do my own data binding because sometimes it's more efficient to do client side processing than to load the server with the work.

I'm sure all of what I've said is easily possible within your framework, but a simple example would go a long way towards helping to make that visible.

Microsoft is moving away from "Magic", and that was an early concern about Signal, and they made a move to make how they did things less hidden.

Ok, enough of the long rant, and one last thing.

I also tried your "Bunny" example with it in IE & Chrome! Chrome kept dying, but when it worked, I noticed that the coordinates of the bunny wasn't the same in both browser... just an FYI!

AnswerRe: Some comments... Pin
dsuryd5-Jan-16 6:44
memberdsuryd5-Jan-16 6:44 
GeneralRe: Some comments... Pin
Dewey5-Jan-16 9:22
memberDewey5-Jan-16 9:22 
Generalstriking a balance between clientside/serverside Pin
Leblanc Meneses5-Jan-16 18:40
memberLeblanc Meneses5-Jan-16 18:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Tip/Trick
Posted 4 Jan 2016

Stats

14.9K views
14 bookmarked