Jaa


Federated Search Connector in Windows 7 with ASP.NET MVC

I was asked to translate this article from Swedish so here it goes:

In my effort to create a somewhat realistic scenario, I’ve now tried to create a federated search-component for Windows 7 based on an ASP.NET MVC aplication. In other words, I’m exposing the possibility to seach content in my ASP.NET MVC application from within Windows 7 explorer. In this article I intend to go through these steps that I’ve taken:

  1. Create the ASP.NET MVC application
  2. Create the ContactController, handling contacts
  3. Create the Search-action, returning RSS
  4. Create the .osdx description to be able to search in Windows 7
  5. Customize search results based on client identity

Before I begin I’ve already downloaded the ASP.NET MVC 1.0 release and also created a database with the following structure:

image

If you want to make it very easy for yourself, I’ve based the schema above on the Contact Management schema which you can find here! Then I’ve added some rows in the database to give me something actual to work with, I leave this task to you.

1. Create the ASP.NET MVC application
I start with creating a new MVC application and name it ContactManagementMVC, I will also generate a unit-test project but will not be using it in this article. I have to make this a priority for myself moving on..

image

I then continue by adding the models I’ll be using, in this example I’ll be using Ling to SQL and hence I add a connection to my database with Visual Studio. Then I add the interesting objects to the .dbml-file as below.

image

The last thing I do in this step is to also copy a bunch of pictures on the people that I have in the database. In my solution I add these to the directory Content/Images, it’s up to you if you feel comfortable publish them somewhere else.

imageThere’s no actual custom logic in the application currently but that will change, starting now…

2. Create the ContactController, handling contacts
Now I create an additional controller by right-clicking on the directory Controllers and choose Add | Controller. I wont be needing and special methods for this purpose, instead I choose to just create an empty controller.

The first thing to do is to add the DataContext object to the controller, giving me the feature of working the database.

ContactManagmentDataContext context = new ContactManagmentDataContext();

Then I implement the Index-method, giving us the option of listing all existing contacts in the database.

public ActionResult Index()
{
    var query = from contact in context.Contacts
                select contact;

    return View(query.ToList<Contact>());
}

This code leverages LINQ to SQL to fetch all contacts from the database and return the view “connected” to the method Index (which we shortly will create). The view will also be passed the list of contacts.

To generate the Index-view, I simply right-click within the method-body and choose "Add View”. Then I choose to create this view strongly typed and viewing the content as a list of Contact objects from the database.

image

The resulting view includes a bit too much autogenerated columns so I erase those that I’m not interested in, do this yourself for the purpose of your application. My result look like the below when I browse the application and enters /Contact in the address-bar.

image

What I also would like to do, is to implement the detail-page. I create another method in the ContactController-class. The following snippet is the entire implemention, in which I simply filter and return another view, this time displaying only the actual contact attached to the view:

 

public ActionResult Details(string id)
{
    var query = from contact in context.Contacts
                where contact.contact_id == int.Parse(id)
                select contact; 

    return View(query.FirstOrDefault<Contact>());
}

Once again I create a view in the same manner as earlier, but this time I choose “Details” instead of “List” as the way to display content. I also change this view to only show the information I’m interested in. Here’s my result:

image

Now it’s time to create the RSS-search function!

3. Create the Search-action, returning RSS
The method itself which creates the feed is pretty simple. I pass an argument which contains the search-parameter to the method and the filter the contacts on title with the following code:

public ActionResult Search(string parameter)

    var query = from contact in context.Contacts
                where contact.job_title.Contains(parameter) 
                select contact; 

    return View(query.ToList<Contact>());
}

The view I create then is kind of special. I don’t want to create an ordinary HTML-page in this case, instead a RSS-feed which either will be read through a browser or a RSS reader. I choose to generate a strongly typed view with the datatype List<Contact> (please observe that the namespace will also be in the dialogue window) with any master page or content.

I then update the automatically generated view to the following:

<%@ Page Language="C#"
         Inherits="System.Web.Mvc.ViewPage<List<ContactManagmentMVC.Models.Contact>>" %>
<%@ Import Namespace="ContactManagmentMVC.Models" %>
<rss version="2.0">
    <channel>
        <title>ASP.NET MVC</title>
        <description>RSS Search Results</description><%
            string host = "";
            if (Url.RequestContext.HttpContext.Request.UserAgent.Contains("Windows-Search"))
            { 
                Uri uri = Url.RequestContext.HttpContext.Request.Url;
                host = string.Format("{0}://{1}:{2}", uri.Scheme, uri.Host, uri.Port);
            }
            foreach (Contact item in ViewData.Model)
            {
                string itemUrl = host + Url.Action(
                    "Details", 
                    new { id = item.contact_id.ToString() }); %>
        <item>
            <title><%= Html.Encode(item.contact_name)%></title>
            <description><%= Html.Encode(item.job_title)%></description>
            <link><%= itemUrl%></link>
            <guid isPermaLink="true"><%= itemUrl %></guid>
            <pubDate><%= DateTime.Now %></pubDate>
            <category><%= item.department%></category>
        </item><%
        } %>
    </channel>
</rss>

Worth noting is how i construct “static”-links to every item with the Url.RequestContext object. This is because I want to be able to click on one of the resulted contacts in the search results and transfer to the webpage. The special “static” link is only interesting if the UserAgent-string contains “Windows-Search” which will indicate that it’s Windows doing the searching and not a browser. This could be a potential way of further more adapting the rendering, but in this case I’m satisfied by the facts that the links generated will work for my application. This is a result of a simple search:

image

The raw xml-document from the RSS-feed looks like this:

<rss version="2.0">
    <channel>
        <title>ASP.NET MVC</title>
        <description>RSS Search Results</description>
        <item>
            <title>Johan Lindfors</title>
            <description>Developer</description>
            <link>/Contact/Details/3</link>
            <guid isPermaLink="true">/Contact/Details/3</guid>
            <pubDate>2009-04-01 08:33:19</pubDate>
            <category>DPE</category>
        </item>
    </channel>
</rss>

4. Create the .osdx description to be able to search in Windows 7
Now we’ve finally come to the “integration” between ASP.NET MVC and Windows 7. What I would like to do is to allow the client running the browser being able to install the connector that’s required to be able to search on the web-solution from Windows. I’d like to do the installation with a link on the webpage that the user can click on. After a bit of searching on the web with Live I’ve finally found a solution I find pretty slick.

I need to dynamically generate the .osdx file required to install the connector, but I don’t want it to be rendered in the browser, instead I want it to automatically be installed (after acceptance from the user). Hence I can’t use an ordinary ActionResult, instead I need to fix the solution with some custom classes and inheritance, nothing fancy or especially difficult but here it goes:

The first thing I do is add a method on my HomeController which I call InstallOpenSearch. It looks like this:

public ActionResult InstallOpenSearch()
{
    ViewData["Name"] = "ASP.NET MVC";
    ViewData["Description"] = "Search provider for ASP.NET MVC";

    return new XmlViewResult();
}

You might observe that I don’t return a view, instead I return an instance of XmlViewResult. This class has been created to handle the generation of a view that’s not rendered, but triggers a download of the file. This example is actually based on the solution provided by Oxite.

public class XmlViewResult : ViewResult
{
    public XmlViewResult()  { } 

    public override void ExecuteResult(ControllerContext context)
    {
        TempData = context.Controller.TempData;
        ViewData = context.Controller.ViewData; 

        base.ExecuteResult(context); 

        context.HttpContext.Response.ContentType = "application/opensearchdescription+xml";
    }
}

Also note that I explicitly tell the ContentType on the response to the client that this should by “application/ópensearchdescription+xml” and not HTML or something else. Still a view will be used and just like when I generated the search result view for RSS, I create another empty view. This one won’t need any type or special content, not even a master page, just as clean as possible. after some manual editing of the view the result became:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<% Uri uri = Url.RequestContext.HttpContext.Request.Url;
   string host = string.Format("{0}://{1}:{2}",uri.Scheme, uri.Host, uri.Port);
   string searchUrl = host + Url.Action("Search","Contact")+"?parameter={searchTerms}";  %>
<OpenSearchDescription xmlns="https://a9.com/-/spec/opensearch/1.1/">
    <ShortName><%= ViewData["Name"] %></ShortName>
    <Description><%= ViewData["Description"] %></Description>
    <Url type="application/rss+xml" template="<%= searchUrl %>" />
</OpenSearchDescription>

Here I also construct dynamically the url to the RSS search in the ASP.NET MVC solution. What I also note now is that I probably should have extracted everything around search to an own controller, but that will have to be a later project.

Now I’d like to give the web user the possibility to install the search connector. I update the Index-view for all contacts to include the following code on a suitable place, possibly just beneath the heading of the page.

<% if (Url.RequestContext.HttpContext.Request.UserAgent.Contains("Windows NT 6.1")){ %>           
<%= Html.ActionLink("Install OpenSearch Connector for Windows!","InstallOpenSearch","Home") %>   
<br /><br />
<% } %>

This will result in a link that directly gives me the installation possibility, but only if I’m running Windows 7.

5. Customize search results based on client identity
The last thing I’d like to evaluate and implement is to filter the search result based on a clients identity, which produces some powerful solutions if integrated in applications, preferably on Intranet solutions.

I start with making sure that I’m leveraging Windows for authentication by updating the web.config and changing <authentication mode="Forms"> to <authentication mode="Windows">.

This makes it possible to update the Search-method as below:

public ActionResult Search(string parameter)
{
    string identityName = GetIdentityName();

    if (identityName != null)
    {
        var query = from user in context.Users
                    from contact in context.Contacts 

                    where contact.user_id == user.user_id
                    where user.account_name == identityName
                    where contact.job_title.Contains(parameter) 

                    select contact; 

        return View(query.ToList<Contact>());
    } 
    else
        return View();           
}

Here I find the authenticated users name and fetches that persons contacts instead of returning them all. I also created a small helper method as below:

private string GetIdentityName()
{
    WindowsIdentity identity = Thread.CurrentPrincipal.Identity as WindowsIdentity;
    if (identity != null)
    {
        return identity.Name;
    }
    return null;
}

In this way I also get a layer of security in the application, even though it’s just for this search-method, but to add this in other places is easy and up to you :)

I’d also like to extend my thanks to Fredrik Normén for some guidance when I was a bit out there during writing of this article. It was his suggestion to stick to the real MVC-pattern and return custom views for the RSS-feed instead of messing around with serialization and rendering within the controller. Thanks!

This is a final view of the result with previews and some additional elements in the RSS-search results, pretty neat, wouldn’t you agree?

image

Comments

  • Anonymous
    April 01, 2009
    PingBack from http://www.anith.com/?p=25552

  • Anonymous
    April 01, 2009
    Excellent!!!  Great article just as I thought :)

  • Anonymous
    April 01, 2009
    Looks great! :-) But what happends if you go to for example "/Contact/Details/lol"? If you have something like this: routes.MapRoute("ProfileDetails", "{id}", new { controller = "Contact", action = "Details" }, new { id = @"d+" } ); If you go to /12345 you will get directly to that customer, and I think you can change this: public ActionResult Details(string id) To this: public ActionResult Details(int id) Just my €0,02, Mikael Söderström

  • Anonymous
    April 05, 2009
    Mikael Söderström: Great suggestion, but since the article is primarily on how to integrate the search feature with Windows 7, I'll leave it as is!

  • Anonymous
    April 06, 2009
    The comment has been removed