BUILD A Visual Studio .NET C# MS Search Webservice
With VB 6.0 COM Interop

By Peter A. Bromberg, Ph.D.
Printer-Friendly Version

Peter Bromberg  

In this article we will create a "Screen Scraper" DOT NET Webservice in C# that allows the user to enter any search term, choose the number of days back to search (3,7,15,or 30 days) and it will go out to the Microsoft Search Site facility, grab the results, and parse them into a SOAP return call containing all the URLs and associated link titles for display. We will also create a custom DHTML client-side page to call the Webservice with a standardized SOAP method call, receive the SOAP Response, parse, and display the results in an HTML Table. Finally, I'll show you the code for a VB 6.0 classic COM component that does the exact same Microsoft Search Service "Screenscrape" as the C# Webservice and we will create a second C# Webservice that illustrates the use of .NET COM Interop to make its method call through the classic COM component instead, and return the identical results.

You will see examples of using the WebRequest and WebResponse Classes, the RegularExpressions Class, use of the StreamReader and StringBuilder Classes, and some examples of various types of string operations using overloaded functions such as IndexOf and Substring. Finally, in the second C# block we will show how to set a reference to a classic COM component in the VS .NET IDE, and how to instantiate the component and call it via COM Interop from directly within your C# code, as well as the use of structured exception handling.



I'm presenting this article because I believe these are some of the kinds of things that developers will want and need to do with the .NET platform, and since I've completed the learning curve (at least this far anyway...), I thought this material should prove to be useful to others. What you can do -- if this or any of our other articles are helpful to you -- is to come back and become a contributor to eggheadcafe.com. If you can't provide an article, a code snippet or other resource for the intermediate - to - advanced level Windows web developer, then consider becoming a contributor to our active Discussion Forums.

I'm not going to go over the basics of how to create a C# Webservice project here, because the Visual Studio .NET IDE basically does all this for you. You just tell it you want a new C# Webservice, and it automatically creates an IIS virtual directory and populates it with all of the file infrastructure you need to say "Hello World" with SOAP.  If you are still writing your C# code in Notepad and using the command -line compilers, TLBIMP.EXE and other tools, I think you're doing yourself a real disservice. For a modest shipping fee Microsoft will send you the whole banana - three CD's containing Beta 2, and it doesn't expire. It's feature complete so there won't be many changes from this point forward. Once you turn on debugging and see how powerful the IDE Debugger is, you'll thank me. I wrote this Webservice using the debugger in probably one - tenth the time it would have taken me without the IDE, simply because I was able to set intelligent breakpoints and watches in the code and simply mouse over variables and see what was happening. You can even change code while the project is running, and the IDE will ask you if you want to rebuild! There are so many other conveniences and new features, I think any developer who intends to explore the .NET platform successfully should learn and use the IDE. One of the first things I did with Beta 2 was to choose "Tools/External Tools" and set up WINCV (the Class Viewer):

 

With this tool at your fingertips, any time you need to do something and you're not sure what base class to use, type it into WINCV and it will show you a two-pane list of all the matching classes on the left, and all the methods on the right. Sure speeds up the learning process when you need to do something and you've never heard of it before! Now let's get on to the project, and as is my custom, we'll do it with an annotated walk through the main project file, MSInfoService.asmx:

// first we define our page language and class
<%@ WebService Language="C#" class="MSInfoService" %>
// import all the base classes we will use ..
using System ;
using System.Web.Services ;
using System.Net ;
using System.IO ;
using System.Text ;
using System.Text.RegularExpressions ;

// Webservice Attribute declaration with description and namespace
[ WebService(Description="MSInfoService", Namespace="http://www.nullskull.com/webservices")]

// class definition / interface - inherits from Webservice
public class MSInfoService : WebService
{
// Webmethod attribute
[WebMethod]

// our function definition / interface
public string GetItems(string sType, string sDays)
{
string result =null;
// set up checks for missing or invalid method parameters and handle appropriate throws
try
{
// Check for missing / invalid parameters
if(sType=="" || sDays == "")
{
throw(new ArgumentNullException("Missing Search Type or Days"));
}

if(sDays != "3" && sDays !="7" && sDays !="15" && sDays !="30")
{
throw(new Exception ("Days:Valid Choices Are 3/7/15/30 days"));
}

//URL to Microsoft Search PRB/HOTFIX/HOWTO and Keywords Search server. Note that here is where we insert our method parameters
string serverURL =@"http://search.support.microsoft.com/kb/psssearch.asp?SPR=msall&KT=ANY&T=N&T1="+sDays+"d&LQ=" + sType +"&PQ=PastQuery&S=T&A=T&DU=C&FR=0&D=support&LPR=&LNG=ENG&VR=http%3A%2F%2Fsupport.microsoft.com%2Fsupport%3Bhttp%3A%2F%2Fsupport.microsoft.com%2Fservicedesks%2Fwebcasts%3Bhttp%3A%2F%2Fsupport.microsoft.com%2Fhighlights&CAT=Support&VRL=ENG&SA=GN&Go.x=21&Go.y=21"; 
// set up some needed variables..
string sTemp="";
string sContentTemp="";
string sContentNew="";
string strChar="";
string chrComma=",";
string chrQuote="\"";
//Create a HttpWebRequest object for the server search URL
HttpWebRequest webreq = (HttpWebRequest)WebRequest.Create(serverURL);
// [Important!] See Note in main article text about these next two lines!!
webreq.MaximumAutomaticRedirections =60;
webreq.UserAgent="Mozilla/4.0 (compatible; MSIE 6.0b;Windows NT 5.0)";
//Retrieve HttpWebResponse object from the Search server URL
HttpWebResponse webresp = (HttpWebResponse)webreq.GetResponse();
//Create StreamReader object and pass the searchserver stream as parameter, with ASCII encoding
StreamReader strm = new StreamReader(webresp.GetResponseStream(), Encoding.ASCII);
//iterate through the stream, building return XML Document string
sContentTemp = strm.ReadToEnd();
strm.Close();
// let's chop off only the part we need to work with and make things easier...

int begpos = sContentTemp.IndexOf("SearchResults[nResults++]=");
int endpos =sContentTemp.IndexOf("function GetFormContents");
sContentTemp=sContentTemp.Substring(begpos,endpos-begpos);
// You could Put this all into a new StringBuilder to use Regex replace and clean it up...
// StringBuilder sbContent = new StringBuilder(sContentTemp);
// Clean this thing up by getting rid of all the TAB and newline characters...
// sbContent.Replace("\n","");
// sbContent.Replace("\t","");
// And Pull it back out to a String to play around with ...
// sContentTemp=sbContent.ToString();
// More working variables
int curbegin;
int tempEndPos;
int iItemEndPos;
string sWorkingChunk="";
string TITItem="";
string finalStuff="";
string URLItem="";

//////////// MAIN ITERATION LOOP ////////////

for (int i = 1;i<100;i++){ // do we figure >100 is enough results?

try {
curbegin=sContentTemp.IndexOf("http://support")-1;
if (curbegin<1) break;
iItemEndPos=sContentTemp.IndexOf(";");
sWorkingChunk=sContentTemp.Substring(curbegin,iItemEndPos);
sContentTemp=sContentTemp.Remove(0,iItemEndPos+1);
tempEndPos = sWorkingChunk.IndexOf(chrComma)-2;
URLItem=sWorkingChunk.Substring(1, tempEndPos);
sWorkingChunk=sWorkingChunk.Remove(1,tempEndPos+2);
tempEndPos = sWorkingChunk.IndexOf(chrComma)-2;
TITItem=sWorkingChunk.Substring(3,tempEndPos-2);
// build our XML for the SOAP return payload
finalStuff += "<ContentItem><URLItem>" + URLItem + "</URLItem><TitleItem>" + TITItem + "</TitleItem></ContentItem>";
}
catch(Exception e)
{
result= e.Message;
}

} // End for
result=finalStuff;
}
catch(Exception e)
{
result= "<Error>" +e.Message+"</Error>";
}
//Return the result XML or Exception
return "<MSInfoResult>" +result +"</MSInfoResult>";
} // End Method
} // End Class

And there is is! Did you see the "[Important!]" comment just before the AutomaticRedirections and UserAgent WebRequest property settings? This Webservice (and many others) will NEVER WORK without these settings! The Microsoft search site is LOADED with "302's", and the default Automatic Redirections for WebRequest is something like 30, which is simply not enough. In addition, this site is optimized for your IE browser. But if you come in with a .NET Webservice call, it doesn't see a browser. So, we have to explicitly provide the correct one, and we do that with the UserAgent property. Ain't that neat? Now let's go ahead and try this puppy out -- it is sitting on our server:

TEST EGGHEADCAFE.COM MSINFO SERVICE WEB SERVICE

Now we will go over the Client - side page. I've used the Microsoft webservice.htc behavior, and frankly, after having it work in BETA 1, it seemed to get broken in BETA 2. In fact, the last time I went up there to download the latest, the link was broken. So let's just write our own client page, and we'll send our own SOAP envelope. Good practice anyway...

<script>
function sendIt(sType,sDays){
var xmlhttp =new ActiveXObject("MSXML2.XMLHTTP.3.0");
var sURL="http://www.nullskull.com/Webservices/MSInfoService/MSInfoService.asmx"
xmlhttp.Open("POST", sURL,false);
xmlhttp.setRequestHeader("SOAPAction", "http://www.nullskull.com/webservices/GetItems");
xmlhttp.setRequestHeader("Content-Type", "text/xml");
var sPayload="<?xml version=\"1.0\" encoding=\"utf-8\"?>"
sPayload+="<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
sPayload+="<soap:Body>"
sPayload+="<GetItems xmlns=\"http://www.nullskull.com/webservices\">"
sPayload+="<sType>" + sType + "</sType>"
sPayload+="<sDays>" + sDays + "</sDays>"
sPayload+="</GetItems>"
sPayload+="</soap:Body>"
sPayload+="</soap:Envelope>"
xmlhttp.Send(sPayload);
status.innerText= "status: " +xmlhttp.status+ "<BR>";
//document.write( xmlhttp.ResponseText);
var xmlDoc=new ActiveXObject("MSXML2.DOMDocument.3.0");
xmlDoc.async=false;
var xslDoc=new ActiveXObject("MSXML2.DOMDocument.3.0");
xslDoc.async=false;
var tmpXML=xmlhttp.responseXML.xml
var re = /&lt;/g;
tmpXML=tmpXML.replace(re,"<");
re = /&gt;/g;
tmpXML=tmpXML.replace(re,">");
//alert(tmpXML);
xmlDoc.loadXML(tmpXML);
var objNode;
var strHTML="<TABLE STYLE=\"border:1px solid black\">"
try {
var errMsg=xmlDoc.getElementsByTagName("Error").item(0).text;
}
catch(e)
{
// no errMsg //
//errMsg="";
}
if(errMsg ==null || errMsg =="")
{
var sURLItemList=xmlDoc.getElementsByTagName("URLItem");
var sTitleItemList =xmlDoc.getElementsByTagName("TitleItem");
// here we loop only if sURLItemList.length>0
if(sURLItemList.length >0)
{
for(var i=0;i<sURLItemList.length;i++)
{
strHTML+="<TR STYLE=\"font-size:8pt;\" BGCOLOR=\"lightblue\" >"
strHTML+="<TD><A HREF=" +sURLItemList.item(i).text +">";
strHTML+=sTitleItemList.item(i).text +"</a></td>";
strHTML+="</TR>"
}
strHTML+="</TABLE>"
}
else
{
strHTML+="<TR><TD>"+errMsg+"</td></tr></table>"
}
result.innerHTML=strHTML;
} else {
result.innerHTML = "No Records found or Invalid Query.";
}
delete xmlhttp;
}
</script>
<BODY>
<CENTER>
<BASEFONT FACE=Verdana>
<TABLE STYLE="border:1px solid black">
<TR STYLE="font-size:10pt;" BGCOLOR="lightblue" >
<TD colspan="2" align="center"><B>Microsoft Search C# WebService - <i>Courtesy of</i></b></TD></TR>
<TR><TD colspan="2" align="center"><img src="http://www.nullskull.com/images/eggheadmasthead2.jpg"></TD></TR>
<TR STYLE="font-size:8pt;" BGCOLOR="lightblue" >
<TD align="right" width=50%><input type=text id=sType size=30 ></TD><TD>Enter Search Words</TD></TR>
<TR STYLE="font-size:8pt;" BGCOLOR="lightblue" ><TD align="right" ><select id=sDays>
<option value=3>3</option>
<option value=7 selected>7</option>
<option value=15>15</option>
<option value=30>30</option>
</select></TD><TD>Choose Number Days Back to Search</TD></tr>
<TR STYLE="font-size:8pt;" BGCOLOR="#FFCC66" ><TD colspan="2" align="center"><input type=button value="Get Results" onClick="sendIt(sType.value, sDays.value);"></TD></TR>
<TR STYLE="font-size:8pt;" BGCOLOR="#FFCC66" ><TD align="center"><A href=http://www.nullskull.com/webservices/msinfoservice/Msinfoservice.asmx>Info Page</a></td><td align="center"><a href=http://www.nullskull.com/webservices/msinfoservice/Msinfoservice.asmx?WSDL>WSDL</a></TD></TR>
</TABLE>
<div id=status></div>
<div id=result></div>
</CENTER>

 

I'm not going to spend a lot of time "handholding" through this code, if you look it over line- by line it should be pretty easy to see what we're doing here. Basically, we build a custom SOAP call with SOAPACTION header and namespace stuff, and send it on its merry way with xmlhttp from the client. Then we just wait for our SOAP response and we deal with it; if it's an error we have some structured exception handling to display an appropriate message, otherwise we grab the correct XML nodelists and use them to create our list of <A HREF links in a table. This is good practice for any developer who wants to understand SOAP - to learn what the pieces all are and what they are for.

Now I want to switch gears and let's do our Webservice all over again but this time we're going to do COM INTEROP. Now I'm not going to bore you with the VB 6.0 ActiveX DLL code that does pretty much what we already did in C# in our Webservice; the VB 6.0 project is included in the ZIP file download for this article, along with working code for everything else.

What I do want to illustrate, however, is how incredibly easy COM Interop is in DOT NET - especially if you do it in the IDE! (Don't you just love the ring of that phrase --, "Incredibly easy"?). First let's Set a COM Reference to our Classic COM Component:

Once that's done, it will show up just like all the other base class references in your Project:


And now, to use this Classic COM component with COM Interop in our C# Webservice, we simply get rid of all the C# code and replace it with this call:

// set a reference to our COM Class..
using MSInfoService;

// Create an instance of the class object
MSInfoService.MSInfo objMSInfo;
objMSInfo = new MSInfoService.MSInfo();
// Call the GetInfo() method
finalStuff= objMSInfo.GetInfo(sType,sDays);
result=finalStuff;

// the rest is pretty much the same as the original...
}
catch(Exception e)
{
result= "<Error>" +e.Message+"</Error>";
}
//Return the result XML or Exception
return result ;
} // End Method
} // End Class

Obviously what this means is that we can do our migration path to 100% .NET a lot more easily. We don't have to "throw away" all the stuff we wrote in VB or VC++, it's really easy to continue to use it.

Webservices in Visual Studio .NET enable companies to really leverage the development time of their programmers, and to provide standards - based interoperability between diverse systems over the wire. COM Interop mean that companies can begin using .NET Webservices NOW, and not have to lose the valuable time and work they've put into Classic COM development.

Download the code that accompanies this article



Peter Bromberg is an independent consultant specializing in distributed .NET solutionsa Senior Programmer /Analyst at in Orlando and a co-developer of the NullSkull.com developer website. He can be reached at info@eggheadcafe.com