För att göra ett någorlunda realistiskt scenario så har jag nu försökt mig på att skapa en federerad sök.komponent till Windows 7 från en ASP.NET MVC applikation. Med andra ord, jag öppnar möjligheten att kunna söka på min ASP.NET MVC-applikation från Windows 7’s Explorer. I den här artikeln tänker jag gå igenom stegen som jag tagit.

  1. Skapa ASP.NET MVC applikationen
  2. Skapa en ContactController som hanterar kontakter
  3. Skapa en Search-“action” som returnerar RSS vid sökning
  4. Skapa en .osdx beskrivning för att kunna söka i Windows 7
  5. Anpassa sökresultaten baserat på klientens identitet

Innan jag börjar så har jag laddat hem ASP.NET MVC 1.0 och även skapat en databas med följande struktur:

image

Om du vill göra det enkelt för dig själv så har jag utgått från schemat “Contact Managment” som du kan hitta här! Sedan har jag lagt till en del poster i databasen så att jag har något att jobba med, det överlåter jag åt dig att själva leka med.

1. Skapa ASP.NET MVC applikationen
Jag börjar med att skapa en ny MVC applikation och döper den till ContactManagementMVC, jag skapar också ett enhets-tests projekt men kommer i den här artikeln inte använda mig av den dessvärre. Jag måste bli bättre på detta…

image

Sedan fortsätter jag med att lägga till de modeller som jag kommer att använda mig av, i det här exemplet använder jag mig av Linq To SQL och lägger alltså till en koppling till min databas med hjälp av Visual Studio. Sedan lägger jag till de intressanta objekten till .dbml-filen enligt nedan:

image

Det sista jag gör i det här skedet är att också kopiera in en del bilder på de personer som jag har i databasen. I mitt fall lägger jag dessa bilder under katalogen Content/Images, men det är upp till dig.

imageÄn så länge har jag ingen speciell logik i applikationen, men det kommer, redan nu…

2. Skapa en ContactController som hanterar kontakter
Nu skapar jag en till “controller” genom att högerklicka på “Controllers” och väljer Add | Controller. Jag behöver inga speciella funktioner för det här ändamålet, istället väljer jag att bara skapa en enkel, tom “controller”.

Det första jag gör att lägger till context-objektet till “controllern” som ger mig möjligheten att jobba mot databasen.

ContactManagmentDataContext context = new ContactManagmentDataContext();

Sedan implementerar jag Index-metoden så att vi kan lista alla befintliga kontakter i databasen.

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

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

Vad koden gör är att hämta alla kontakter från databasen och returnera den vy som är kopplad till metoden Index (som snart ska skapas). Vyn kommer också att skickas en lista av alla kontakter.

För att skapa en vy till detta, högerklickar jag helt enkelt inne i metod-kroppen och väljer “Add View”. Sedan väljer jag att skapa en starkt-typad vy som ska skapa en lista över Contact-objekten från databasen.

image

Den här vyn innehåller lite för mycket automatgenererade kolumner så jag raderar det som jag inte är intresserad av, gör det själv för att passa ditt ändamål. Så här ser resultatet ut för min del när jag “surfar” till webblösningen och skriver /Contact i adressfältet.

image

Vad jag också vill göra är att implementera detaljsidan och skapar därför ytterligare en metod i ContactController-klassen. Följande är hela implementationen, där jag helt enkelt filtrerar och returnerar en vy med bara den aktuella kontakten kopplad till den:

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>());
}

Återigen skapar jag en vy på samma sätt som tidigare, men väljer istället för “List” att nyttja mallen för “Details”. Jag ändrar även i den här vyn så att jag bara visar den information som jag är intresserad av. Så här blev resultatet för mig:

image

Nu har det blivit dags att skapa sökfunktionen som ska returnera RSS.

3. Skapa en Search-“action” som returnerar RSS vid sökning
Själva metoden som skapar feeden som ska returneras är ganska enkel. Jag skickar ett argument med sökparametern till metoden och filtrerar sedan mina kontakter på titel enligt följande:

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>());
}

Min vy som sedan genereras blir lite speciell. Jag vill inte skapa en vanlig HTML sida i det här fallet utan istället en RSS-vy som  antingen läses i webbläsaren eller av en RSS-läsare. Därför väljer jag att generera en starkt typad vy med datatypen List<Contact> (observer att namnrymden också står i dialogrutan) utan någon som helst “MasterPage” eller speciellt innehåll.

Den vy som då skapas åt mig uppdaterar jag manuellt så att den till slut ser ut som följer:

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

Värt att notera kan vara hur jag bygger upp “statiska” länkar till respektive objekt med hjälp av Url.RequestContext. Det är för att jag i sök-resultatet ska kunna klicka mig direkt till rätt objekt på webbsidan. Det gör jag visserligen bara om UserAgent-strängen innehåller “Windows-Search” vilket identifierar att det är Windows som söker och inte någon webbläsare. Det här kan vara ett sätt att ytterligare påverkar renderingen, men i det här fallet så nöjer jag mig med att se till att länkarna som skapas är korrekta för min applikation. Resultatet av en sökning kan vara följande:

image

Och den råa RSS-texten ser ut så här:

<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. Skapa en .osdx beskrivning för att kunna söka i Windows 7
Nu har vi äntligen kommit till integrationen mellan ASP.NET MVC och Windows 7 (om jag får kalla det för integration). Vad jag vill göra är att låta användaren av webbläsaren kunna installera den konnektor som behövs för att kunna söka på webbsidan från Windows och det gärna med hjälp av en länk som användaren kan klicka på. Efter lite sökandes på webben med Live så hittar jag en lösning som jag själv tycker är ganska galant.

Vad jag behöver göra är att dynamiskt generera den .osdx fil som behövs för att kunna installera konnektorn, men jag vill inte att den ska renderas i webbläsaren utan automatiskt installeras (efter att användaren godkänt såklart). Därför kan jag inte använda ett vanligt ActionResult utan behöver pilla med lite klasser och arv, inget jätteavancerat men så här blev det.

Först så lägger jag till en metod på min HomeController som jag döper till InstallOpenSearch. Den ser ut som följer:

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

    return new XmlViewResult();
}

Observera här att jag inte returnerar en vy utan istället en instans av XmlViewResult. Detta är en klass som jag skapat för att hantera generering av en vy som inte renderas utan istället triggar en nedsparning av filen. Det här exemplet är faktist baserat på det som Oxite använt i sin implementation av en liknande funktion.

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";
    }
}

Observera att jag explicit säger till ContentType på svaret till klienten att detta ska vara en “application/opensearchdescription+xml” och inte HTML eller något annat. Fortfarande så kommer en vy att användas och precis som när jag genererade sökresultatet för RSS så skapar jag ytterligare en tom vy, den här vyn behöver inte ens någon typ skickad till sig utan ska vara så ren som det bara går. Efter lite manellt editerande av vyn så blev resultatet följande:

<%@ 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="http://a9.com/-/spec/opensearch/1.1/">
    <ShortName><%= ViewData["Name"] %></ShortName>
    <Description><%= ViewData["Description"] %></Description>
    <Url type="application/rss+xml" template="<%= searchUrl %>" />
</OpenSearchDescription>

Även här bygger jag dynamiskt upp url’n till ASP.NET MVC lösningens RSS-sökning. Det jag noterar nu är exempelvis att jag kanske borde ha brytit ut hela sökningen till en egen Controller, men det får bli ett senare projekt.

Nu vill jag ge besökaren av webbapplikationen möjligheten att installera sökfunktionen så jag uppdaterar Index-vyn för kontakter till att innehålla följande kod på ett lämpligt ställe, förslagsvis under titeln på sidan:

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

Det kommer resultera i en länk som direkt ger mig möjligheten att installera sökfunktionen, men bara om jag kör Windows 7.

5. Anpassa sökresultaten baserat på klientens identitet
Det sista jag tänkte göra i den här artikeln är att filtrera sökresultatet baserat på en användares identitet vilket gör att den här funktionen blir mycket attraktiv på intranät-lösningar (i det här fallet).

Jag börjar med att se till att jag använder Windows som autentiseringsmetod genom att uppdatera web.config och byta <authentication mode="Forms"> till <authentication mode="Windows">.

Då kan jag i Search metoden exempelvis uppdatera koden som nedan:

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();           
}

Det första jag gör är att hämta den autentiserade användarens namn. Därför skapade jag en liten hjälp metod som ser ut så här:

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

Sedan utökar jag LINQ-frågan att också filtrera så att bara det användarnamn som används slås upp i Users-tabellen och bara kontakter som är relaterade till det användarnamnet kommer att returneras.

På så sätt får jag även ett skikt av säkerhet i applikationen, dock bara i den här sök-funktionen, men att lägga till den på andra ställen enkelt och upp till dig :)

Jag vill också passa på att tacka Fredrik Normén för lite guidning när jag var ute på hal is under skrivandet av den här artikeln, det var hans förslag att följa MVC-mönstret och returnera skräddarsydda vyer iställer för att skapa serialiseringen och renderingen i Controller-klassen. Tack!