|
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="https://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="https://www.nullskull.com/Webservices/MSInfoService/MSInfoService.asmx"
xmlhttp.Open("POST", sURL,false);
xmlhttp.setRequestHeader("SOAPAction", "https://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=\"https://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 = /</g;
tmpXML=tmpXML.replace(re,"<");
re = />/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="https://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=https://www.nullskull.com/webservices/msinfoservice/Msinfoservice.asmx>Info
Page</a></td><td align="center"><a href=https://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.
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
|