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); }
}
private List<MovieRecord> _queryTest = new List<MovieRecord>();
private int _errorPos;
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;
}
}
private List<MovieRecord> Paginate(IEnumerable<MovieRecord> iQueryResults)
{
}
private void AutoComplete(ref string iQuery)
{
}
}
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">
<!--
<div class="input-group">
<span class="input-group-addon">Search:</span>
<input class="form-control"
type="text" data-bind="textInput: Query" />
</div>
<!--
<div class="label label-danger pull-right"
data-bind="text: QueryError"></div>
</div>
<!--
<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>
<!--
</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.