ASP.Net MVC2 and Full-Text Searching

Implementing searching on your site has never been easier. There are many tools out there such as Lucene.Net which is amazing, but if the scale of the project is not too large, it’s probably much easier to enable Full-Text Search on the tables and query results out of the catalog.

I am using ASP.Net MVC2, which provides much flexibility and is amazing fun to work with. Things can be set up to your taste as long as you follow the conventions laid out. Here’s how I have it set up.

Model
I used the following model to retrieve the search text and store the results once they were queried and parsed from the database using a searching service I am going to detail in a bit.

public class SearchEntry
    {
        [Required]
        [DisplayName("Search Keyword(s)")]
        public string QueryText { get; set; }

        public IList<Product> ResultProductsList { get; set; }
    }

The annotations [DisplayName("Search Keyword(s)")] and [Required] help display and validate the model. These can be found in System.ComponentModel and System.ComponentModel.DataAnnotations respectively.

Views
Again, how you want to set the views up is up to you. I have two views and a partial view. One view as a home page, for example. Another to display the results. The partial view is a form for searching which I can reuse as I need to.

The partial view is strongly typed to the SearchEntry model.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.SearchEntry>" %>

    <% using (Html.BeginForm("Submit", "Search", FormMethod.Get)) {%>
                Enter keywords to search:
                <%: Html.TextBoxFor(model => model.QueryText) %>
                <input type="submit" value="Search" />
                <%: Html.ValidationMessageFor(model => model.QueryText,
                                              "At least one search keyword is required") %>
    <% } %>

Pretty straight forward!
The two views are really straight forward too. The home page for example, just renders the form by calling an action called SearchForm defined in the Search controller. You can change that around for sure.
Instead of using Html.RenderAction which is found in System.Web.Mvc.Html you can also use Html.RenderPartial too, depending on how you like your coffee, and needs.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	Home
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <% Html.RenderAction("SearchForm", "Search"); %>

</asp:Content>

The results view may look any way you would like it to be. Again, this view is strongly typed to SearchEntry.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Model.SearchEntry>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Results for "<%: Model.QueryText %>"
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <% Html.RenderPartial("SearchForm"); %>

    <table>
        <thead>
            <tr>
                 <th>Name</th>
                 <th>Price</th>
            </tr>
          </thead>
          <tbody>
          <% foreach (var record in Model.ResultProductsList) { %>
              <tr>
                  <td><%: record.Name %></td>
                  <td><%: record.Price %></td>
              </tr>
           <% } %>
           </tbody>
    </table>
</asp:Content>

Controller
The controller is quite elegant, I find, it does little but accomplishes a lot. It simply glues everything together.

ISearchEngine searchEngine;

//initializes the search engine
public SearchController(IContextRepository contextRepository)
{
    searchEngine = new SearchEngine(contextRepository);
}

//returns the partial view
public ActionResult SearchForm()
{
    return View();
}

//retrieves results and returns the results to view
public ActionResult Submit(SearchEntry searchEntry)
{
    searchEntry = searchEngine.Search(searchEntry);
    return View("Results", searchEntry);
}

I like to retrieve the connection string from what I call ContextRepository. It simplifies things, I find.
The Submit method takes in the model submitted by the search form above and retrieves the results from the search engine service shown below.

Service

public class SearchEngine : ISearchEngine
{
        private IContextRepository ContextRepository;

        public SearchEngine(IContextRepository contextRepository)
        {
            ContextRepository = contextRepository;
        }

        public SearchEntry Search(SearchEntry searchEntry)
        {
            searchEntry.ResultProductsList =
                ProductsFullTextSearch(searchEntry.QueryText, ContextRepository.MasterContext).ToList();

            return searchEntry;
        }

        private IEnumerable<Products> ProductsFullTextSearch(string text, DataContext context)
        {
            return (GenericFullTextSearch<Products>(text, context) as IEnumerable<Products>);
        }

        private IEnumerable<TSource> GenericFullTextSearch<TSource>(string text, DataContext context)
        {
            //Find LINQ Table attribute
            object[] info = typeof(TSource).GetCustomAttributes(typeof(System.Data.Linq.Mapping.TableAttribute), true);
            //Get table name
            String table = (info[0] as System.Data.Linq.Mapping.TableAttribute).Name;
            //Full text search on that table
            return context.ExecuteQuery<TSource>(String.Concat("SELECT * FROM ", table, " WHERE CONTAINS(*, {0})"), text);
        }
}

That’s all! I’m sure a lot can be improved on. On it now!

Advertisements

Multiple Datepickers

Using ASP.NET MVC with Javascript is like a breeze and really quite useful too. I needed to have several date fields in a form for the project I am currently working on. I found jQuery UI to be a great and very pretty solution and thought I might share.

First I got the jQuery UI and placed it in the Content folder of the MVC project. I had put my script in a file called datepicker.js also in the Content folder. They can be anywhere else too, depending on that the following paths will have to be changed. Furthermore the jQuery JavaScript library will also be needed. It’s already there in the MVC project’s Scripts and jQuery UI’s js folder.

Referring to them all:

<link type="text/css" href="<%= Url.Content("~/Content/jqueryUI/css/smoothness/jquery-ui-1.7.2.custom.css") %>" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="<%= Url.Content("~/Script/jquery-1.3.2.min.js") %>"></script>
<script type="text/javascript" src="<%= Url.Content("~/Content/jqueryUI/js/jquery-ui-1.7.2.custom.min.js") %>"></script>
<script src="<%= Url.Content("~/Content/datepicker.js") %>" type="text/javascript"></script>

I placed a wildcard in the datepicker function to pick up any element with date in its id:

$(function() {
$("*[id*=date]").datepicker();
});

Now in the form that contains the date fields:

            <p>
                <label for="DateStarted">Date Started:</label>
                <%= Html.TextBox("DateStarted", String.Format("{0:d}", Model.DateStarted), new { @id = "date1" })%>
                <%= Html.ValidationMessage("DateStarted", "*") %>
            </p>
            <p>
                <label for="TerminationDate">Termination Date:</label>
                <%= Html.TextBox("TerminationDate", String.Format("{0:d}", Model.TerminationDate), new { @id = "date2" })%>
                <%= Html.ValidationMessage("TerminationDate", "*") %>
            </p>

I added the id attribute to each text box that was meant as a date field. It’s important to specify date1 and date2 (in lines 3 and 8) or other unique names containing date because the wildcard would cause any name with date in it to register as elements for the datepicker to be applied on.

Hope it will be useful for someone, it surely was for me.