Geschrieben von: Christoph Wille
Kategorie: Web Services
This printed page brought to you by AlphaSierraPapa
Wenn Google seinen Suchkatalog per Web Service anbietet, warum sollte man dann nicht seine lokale Sitesuche per Web Service den Programmierern öffnen? Heute werden wir einen solchen Suchservice basierend auf dem Index Server, der ja Teil von Windows 2000 ist, implementieren. Und damit man die Nützlichkeit eines solchen Services sieht, erstellen wir ihn für AspHeute.com, und geben auch noch einen Windows-Client mit dazu.
Es gibt bis dato keine managed Klassen um auf die Index Server Catalogs zugreifen zu können. Daher müssen wir den Weg mit COM InterOp wählen, und wie im Artikel Objektbasierte Index Server Suche die Index Server Search Objects (IXSSO) einsetzen. Damit erhalten wir die volle Suchfunktionalität von Index Server, die auch sehr gut in Integrating Other Server-Side Technologies beschrieben ist.
Unser erster Schritt ist ein leeres Web Service Projekt in Visual Studio.NET zu erstellen (Hinweis: natürlich kann man diesen Web Service auch ohne VS.NET erstellen). Diesem fügen wir die COM Referenz für die Index Server Search Objects hinzu:
Wir erhalten einen RCW - einen Runtime Callable Wrapper (siehe auch Artikel Verwenden von COM Komponenten in ASP.NET). Diesen können wir im Object Browser betrachten:
Im Interface IixssoQuery sind die für uns vordringlich wichtigen Eigenschaften und Methoden beheimatet, so auch die CreateRecordset Methode, die die Query ausführt und die Resultate zurückliefert. Das interessante daran ist, daß die Resultate als ADO Recordset geliefert werden, wir also eine weitere COM Referenz in unserem Projekt benötigen:
Hierbei handelt es sich um eine sogenannte Primary Interoperability Assembly, das heißt, sie wird vom Hersteller (Microsoft in diesem Fall) geliefert, und ist im GAC abgelegt. Einige Internet Provider sperren den GAC jedoch, sodaß es notwendig sein kann, diese Assembly aus dem GAC der lokalen Maschine herauszukopieren und ins bin Verzeichnis der Website zu installieren (dazu mehr später).
Damit hätten wir alle Vorbereitungen abgeschlossen, und wir können an das Schreiben des Web Service Codes gehen:
using System; using System.Collections; using System.ComponentModel; using System.Diagnostics; using System.Web; using System.Web.Services; using System.Data; using System.Data.OleDb; namespace Suche { [WebService(Namespace="http://microsoft.com/webservices/")] public class AspHeuteSearch : System.Web.Services.WebService { private static string CiCatalog = "aspheute.com"; private static string CiScope = "/artikel/"; private static string CiFlags = "SHALLOW"; // "DEEP" not needed in CiScope public AspHeuteSearch() { InitializeComponent(); }
Zu den standardmäßigen using Statements hat sich System.Data.OleDb dazugesellt - den Grund sehen wir später im Code. Es folgt die Klasse AspHeuteSearch, und einige statische Variablen, die nicht veränderbare Werte halten, zB den Namen des Index Server Catalogs, das Suchverzeichnis und die Suchtiefe. Mit diesen Definitionen können wir daran gehen, unsere beiden Suchmethoden zu implementieren:
[WebMethod] public DataSet KeywordSearch(string Keyword, int MaxResults) { if (MaxResults < 5 || MaxResults > 250) throw new ArgumentOutOfRangeException("MaxResults", MaxResults, "Range is 5-250"); if (Keyword.Length < 3) throw new ArgumentOutOfRangeException("Keyword", Keyword, "Minimum length of search keyword is 3 characters"); return PerformQuery("@ALL " + Keyword, MaxResults); } [WebMethod] public DataSet IXAdvancedQuery(string Query, int MaxResults) { if (MaxResults < 5 || MaxResults > 250) throw new ArgumentOutOfRangeException("MaxResults", MaxResults, "Range is 5-250"); return PerformQuery(Query, MaxResults); }
Einerseits ist das KeywordSearch und andererseits IXAdvancedQuery. Der Unterschied ist, daß erstere eine einfache Schlüsselwortsuche erlaubt, wohingegen die zweite die gesamte Suchsyntax des Index Servers bis hin zu Regular Expressions erlaubt, der Expertenmodus sozusagen. Beide Methoden checken die übergebenen Parameter, und werfen nötigenfalls Exceptions falls der Input nicht den Erwartungen entspricht ("All input is evil until proven otherwise"). Gemeinsam ist beiden der Aufruf von PerformQuery:
private DataSet PerformQuery(string strQuery, int nMaxResults) { Cisso.CissoQueryClass cqc = new Cisso.CissoQueryClass(); cqc.Catalog = CiCatalog; cqc.MaxRecords = nMaxResults; cqc.CiScope = CiScope; cqc.CiFlags = CiFlags; cqc.Query = strQuery; cqc.Columns = "Vpath,DocTitle,Characterization,Rank"; cqc.SortBy = "Rank[d]"; ADODB.Recordset rsIX = (ADODB.Recordset)cqc.CreateRecordset("nonsequential"); OleDbDataAdapter daConvertToDataset = new OleDbDataAdapter(); DataSet myDS = new DataSet(); daConvertToDataset.Fill(myDS, rsIX, "IXResults"); return myDS; }
Diese Methode ist das "Arbeitstier" unseres Webservices. Es erstellt ein Objekt vom Typ CissoQueryClass, und weist alle Eigenschaften zu, die man für die Ausführung der CreateRecordset Methode benötigt. In dieser Methode wird auch der Grund ersichtlich, warum wir den System.Data.OleDb Namespace eingebunden haben - der OleDbAdapter bietet eine performante Möglichkeit, ein ADO Recordset in ein ADO.NET DataSet umzuwandeln. Und dieses DataSet wird zum Client geschickt, der es dann weiterverarbeiten kann.
Bevor wir soweit sind, möchte ich noch das eine oder andere Wort über das Deployment dieses Web Services verlieren. Abgesicherte Web Server eines Providers unterscheiden sich von den "weltoffenen" Entwicklungsmaschinen, die VS.NET installiert haben. Deshalb muß man die web.config seinen Bedürfnissen anpassen:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <compilation defaultLanguage="c#" debug="true"> <assemblies> <add assembly="Suche" /> <add assembly="InterOp.Cisso" /> <add assembly="ADODB"/> </assemblies> </compilation> <customErrors mode="Off" /> <trace enabled="false" requestLimit="10" pageOutput="false" traceMode="SortByTime" localOnly="true" /> </system.web> </configuration>
Der wichtige Punkt hier ist der Abschnitt assemblies - hier definiert man, welche Assemblies in dieser Web Applikation geladen werden sollen. Klarerweise benötigt man die Assembly des Web Services. Ebenso wichtig sind aber auch die Assemblies für COM InterOp von IXSSO und ADODB. Die Assembly adodb.dll kann man aus dem GAC kopieren, sie ist im Download des Web Services im deploy Folder mit dabei. Damit ist der Web Service lauffähig und kann verwendet werden.
Den ersten Test kann man natürlich online im Web Browser durchführen, indem man die Adresse
http://www.aspheute.com/suche/aspheutesearch.asmx
eintippt. Damit gelangt man zur autogenerierten Seite des Webservices und kann beide Suchmethoden einmal durchtesten:
Interessanter ist natürlich ein vollständiger Client, der einige nette Features bieten kann:
Dieses Programm stellt Suchergebnisse in einer Listview dar. Klickt der Anwender auf einen Artikel, wird das Abstract angezeigt. Ist der Artikel interessant für den Benutzer, klickt er auf "View Article!", und das Programm lädt den Artikel im Internet Explorer (man könnte dies natürlich in einer weiteren Ausbaustufe in die Applikation integrieren).
Der erste Schritt bei der Erstellung einer Clientapplikation ist es, den Web Service zu referenzieren, und zwar mittels Add Web Refernce:
Das Erstellen des User Interfaces wird nicht gezeigt, interessant hingegen ist der Code der ausgeführt wird, wenn der Benutzer auf den "Search!" Button in der Applikation klickt:
private void btnSearch_Click(object sender, System.EventArgs e) { string strKeyword = txtKeyword.Text; AspHeute.AspHeuteSearch ahs = new AspHeute.AspHeuteSearch(); DataSet ds = ahs.KeywordSearch(strKeyword, 50); DataTable dt = ds.Tables[0]; lvResults.BeginUpdate(); txtUrl.Text = ""; lvResults.Items.Clear(); foreach (DataRow dr in dt.Rows) { ListViewItem lvi = new ListViewItem(); // Vpath,DocTitle,Characterization,Rank -> order of columns in table lvi.Text = dr[1].ToString(); lvi.SubItems.Add(dr[0].ToString()); lvi.SubItems.Add(dr[2].ToString()); lvResults.Items.Add(lvi); } lvResults.EndUpdate(); MessageBox.Show("Search is complete - " + dt.Rows.Count.ToString() + " results returned from search", "Search AspHeute.com"); }
Der Aufruf des Services sind nur zwei Zeilen (zugegebenermaßen fehlt Exception Handling). Der Rest des Codes befasst sich mit dem Befüllen der Listview basierend auf den Resultaten der Suche. Die Daten für URL und Abstract werden in unsichtbaren SubItems der Listview versteckt - ausgelesen werden diese im SelectedIndexChanged Event:
private void lvResults_SelectedIndexChanged(object sender, System.EventArgs e) { if (lvResults.SelectedItems.Count == 0) return; ListViewItem lvi = lvResults.SelectedItems[0]; txtSummary.Text = lvi.SubItems[2].Text; // show summary text txtUrl.Text = lvi.SubItems[1].Text; // define hyperlink of document }
Bei txtUrl handelt es sich um eine versteckte Textbox, die dann auch zum Anzeigen des Artikels im Internet Explorer verwendet wird:
private void btnViewArticle_Click(object sender, System.EventArgs e) { if (txtUrl.Text != "") Process.Start("http://www.aspheute.com" + txtUrl.Text); }
Für Process.Start benötigt man noch den System.Diagnostics Namespace, aber das war dann schon der gesamte Code für den Windows-Client für unseren Web Service. Der Client ist natürlich ausbaufähig: ein asynchroner Aufruf des Web Services wäre wünschenswert, ein Paging durch die Resultate, ein integrierter Web Browser, etc.
Für den Web Service gibt es einen Kritikpunkt in punkto Interoperabilität: er liefert ein DataSet zurück, das an .NET gebunden ist. Hier sollte man auf ein Array von Result-Objekten umstellen, was nicht weiter schwierig ist - aber ein wenig Programmieraufwand bedeutet.
Ein kleiner Performancetip am Ende - die Index Server Search Objects (IXSSO) erlauben Paging mittels Querystrings - dies kann die Performance des ganzen Web Services deutlich verbessern. Der Web Service müßte nur dahingehend verändert werden, daß anstatt des MaxRecords Parameters ein String als "Bookmark" übergeben wird, der diesen Querystring emuliert.
This printed page brought to you by AlphaSierraPapa
Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20021107.zip
Das DataTable Objekt in ADO.NET
http:/www.aspheute.com/artikel/20001116.htm
Das using Schlüsselwort
http:/www.aspheute.com/artikel/20020318.htm
Datenbankzugriff mittels ADO.NET
http:/www.aspheute.com/artikel/20001102.htm
Einstellungssache - Applikationsdaten in web.config
http:/www.aspheute.com/artikel/20011122.htm
Exception Handling in C#
http:/www.aspheute.com/artikel/20000724.htm
Objektbasierte Index Server Suche
http:/www.aspheute.com/artikel/20010403.htm
Programmieren mit den Google Web APIs Beta 2
http:/www.aspheute.com/artikel/20020415.htm
Verwenden von COM Komponenten in ASP.NET
http:/www.aspheute.com/artikel/20000828.htm
Web Services 101 in ASP.NET
http:/www.aspheute.com/artikel/20010621.htm
Web Services in Anwendungen konsumieren
http:/www.aspheute.com/artikel/20010622.htm
Web.Config 101
http:/www.aspheute.com/artikel/20010802.htm
Integrating Other Server-Side Technologies
http://www.microsoft.com/mspress/books/WW/sampchap/4072a.asp
©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.