The needs
A phone call came here from a good friend of mine. Jean-René Roy, president of the Montreal FoxPro User Group, wanted to improve the flexiblity of their user group Web site by taking advantages of the Universal Thread User Group Meeting Tracker data.
The vision was simple: "Why would I want to enter the data twice within two different environments when I could benefit of that from one and only place?" Then, suddenly, within a few seconds, it came as a reality. "Oh, by using the UGMT data, that mean we could then also be able to improve our Web site content by getting additional related content."
Then, as dynamic as my vision could be, I replied: "Huuummmm, that means you will always have your data up to date as whenever a speaker would update his biography and his picture, for example, on the Universal Thread, you would have it updated immediately on your Web site." Interesting, isn't it?
Technology used
The Universal Thread makes use of various technologies in order to deliver and respond to its client needs. Originally all done in Visual FoxPro, the product has constantly evolved over the years and now offers and makes use of some .NET technology as well. Relying on solid frameworks, such as West Wind Web Connection on Fournier Transformation's own, the Universal Thread represents a great example of breaking the barrier between VFP & .NET in order to benefit of the strenght of both environments.
Web Service
Among its infrastructure, the Universal Thread, since about a year, has migrated its Web Service to .NET. The migration was done within a few days. The advantages of maintaing versions that .NET offers over VFP is great. No longer do I have the need to go on the server or to build some kind of automation in order to upload a new version of the Web Service into the server. Once ready, a simple FTP of two DLL files is necessary in order to have the new version live.
Once a new version is sent to the server, on the first hit, it will take a few additional seconds for the server to recognize and load the new version. But all that is done automatically. So, it's not a big deal.
The intellisense of the VS.NET environment within the Web Service project provides great benefits. The availability of the server objects in the development environment allows many great things such as the ability to see the properties and methods of the server request object, for example, such as HttpContext.Current.Request.ServerVariables("REMOTE_ADDR").
The venue of the Universal Thread Web Service also allowed us to respond to additional needs. Our returned data, available as a XML string can also be requested as a DataSet.
A .NET Web Service also allows an easier mechanism of authentication such as we do on the Web by the use of cookies. A simple command such as System.Web.HttpContext.Current.Response.Cookies("Session").Value = "1234" would be enough to enable a cookie which would be available on all upcoming hits.
Note that .NET users need to instantiate the cookie container in order to have it encapsulated in the environment on all upcoming requests. This can be done by the following command:
' Create the cookie container loUT.CookieContainer = New System.Net.CookieContainer
The following represents an example of two methods of the Web Service which contains their equivalent when needed to send a DataSet:
<WebMethod()> _ Public Function GetUserGroupMeeting(ByVal tdStartDate As Date, ByVal tdEndDate As Date, _ ByVal tnUserGroup As Integer) As String Return Me.GetData("1," + Me.GetDate(tdStartDate) + "," + Me.GetDate(tdEndDate) + "," + _ tnUserGroup.ToString, 5) End Function <WebMethod()> _ Public Function GetUserGroupMeetingDataSet(ByVal tdStartDate As Date, ByVal tdEndDate As Date, _ ByVal tnUserGroup As Integer) As DataSet Return Me.GetDataSet("1," + Me.GetDate(tdStartDate) + "," + Me.GetDate(tdEndDate) + "," + _ tnUserGroup.ToString, 5) End Function <WebMethod()> _ Public Function GetUserGroupSpeaker(ByVal tdStartDate As Date, ByVal tdEndDate As Date, _ ByVal tnUserGroup As Integer) As String Return Me.GetData("5," + Me.GetDate(tdStartDate) + "," + Me.GetDate(tdEndDate) + "," + _ tnUserGroup.ToString, 5) End Function <WebMethod()> _ Public Function GetUserGroupSpeakerDataSet(ByVal tdStartDate As Date, ByVal tdEndDate As Date, _ ByVal tnUserGroup As Integer) As DataSet Return Me.GetDataSet("5," + Me.GetDate(tdStartDate) + "," + Me.GetDate(tdEndDate) + "," + _ tnUserGroup.ToString, 5) End Function
' This query the Universal Thread server for specific XML transactions ' expC1 Query ' expN1 Web Service process to call ' 1 Universal Thread ' 2 Federated Community ' 4 .NET Zone ' 5 Visual FoxPro Zone ' 6 User Group Meeting Tracker Private Function GetData(ByVal tcQuery As String, Optional ByVal tnProcess As Integer = 1) As String Me.CheckLogin() Dim lcServer As String Dim lcHtml As String lcServer = Me.cUrl + tnProcess.ToString() + "," + Me.cUser + "," lcHtml = GetXML(lcServer + tcQuery) Me.CheckForError(lcHtml, False) Return lcHtml End Function
' This query the Universal Thread server for specific XML transactions ' Is it similar to GetData but it returns a DataSet ' expC1 Query ' expN1 Web Service process to call ' 1 Universal Thread ' 2 Federated Community ' 4 .NET Zone ' 5 Visual FoxPro Zone ' 6 User Group Meeting Tracker Private Function GetDataSet(ByVal tcQuery As String, Optional ByVal tnProcess As Integer = 1) _ As DataSet Me.CheckLogin() Dim lcServer As String Dim lcHtml As String lcServer = Me.cUrl + tnProcess.ToString() + "," + Me.cUser + "," lcHtml = GetXML(lcServer + tcQuery) Me.CheckForError(lcHtml, False) Return ImportXML(lcHtml) End Function
' Get the XML ' expC1 Url Private Function GetXML(ByVal tcUrl As String) As String Dim loReq As System.Net.WebRequest Dim lcHtml As String loReq = System.Net.WebRequest.Create(tcUrl) Dim loResponse As System.Net.WebResponse = loReq.GetResponse() Dim loStream As Stream = loResponse.GetResponseStream() Dim loStreamReader As StreamReader = New StreamReader(loStream) lcHtml = loStreamReader.ReadToEnd() Return lcHtml End Function
As the engine called to process the business and data objects is resident in memory, the database connection, the opening of tables and the initialization of many objects is already done once its receive the Web Service request. So, as the environment is already loaded, the respond time is excellent.
The client application
The need on the client side was to build an ASP.NET application to control the Web site. Within that context, it was easy to implement the various requests the user group needed.
A user group project was created and the codehind was built in VB.NET. A reference to the Universal Thread Web Service was added and the creation of several aspx pages was done in order to grab the content for the upcoming meetings, the previous meetings and the speakers list in regards to the MFUG user group.
Many aspx pages are calling the same function in order to display the speakers profiles. So, the method was removed from each related aspx page and placed into a Function.vb file defined as Shared so it is visible in all aspx pages. In fact, the speakers profile is one beauty of this service. It is the most used function and it can be used for various purposes such as listing all speakers who have already presented for the user group or simply to be used to list the board of directors.
In order to deliver a speaker's profile, the speaker need to have a Universal Thread account and update the related information. From the account setup, under Identification and Biography, the speaker will find everything needed to update his profile. The related fields are the company name, the email, the Web site and the biography. The biography can be entered in English, French, Spanish and Portuguese.
The following steps can be executed in order for a speaker to update his profile on the Universal Thread:
Here is an example of a speaker's profile:
Here is the AddSpeaker() method which is the standard now used on several Web sites such as the Universal Thread, the Universal Thread Magazine, the Montreal FoxPro User Group, the Montreal SQL Server User Group and the DevTeach Web sites:
Public Class _Function ' Add a speaker ' expN1 Speaker ID ' expL1 If the speaker picture is available ' expC1 Speaker name ' expC2 Speaker bio ' expC3 Speaker company ' expC4 Speaker email ' expC5 Speaker url Public Shared Function AddSpeaker(ByVal tnSpeakerID As Integer, ByVal tlPicture As Boolean, _ ByVal tcName As String, ByVal tcBio As String, ByVal tcCompany As String, _ ByVal tcEmail As String, ByVal tcUrl As String) As String Dim lcHtml As String Dim lcUniversalThread As String lcHtml = "" ' Universal Thread url lcUniversalThread = "http://www.universalthread.com/" If tnSpeakerID > 0 Then lcHtml = lcHtml + "<TABLE CELLSPACING=0 CELLPADDING=0>" lcHtml = lcHtml + "<TR VALIGN=TOP>" lcHtml = lcHtml + "<TD ALIGN=CENTER WIDTH=90>" If tlPicture Then lcHtml = lcHtml + "<IMG SRC=" + lcUniversalThread + "Photo/" + _ tnSpeakerID.ToString().PadLeft(6, "0") + ".jpg>" Else lcHtml = lcHtml + "<IMG SRC=" + lcUniversalThread + "Photo/PictureNotAvailable.jpg>" End If lcHtml = lcHtml + "<TD> " lcHtml = lcHtml + "<TD WIDTH=100% Class=Normal12>" lcHtml = lcHtml + "<TABLE CELLSPACING=0 CELLPADDING=0>" lcHtml = lcHtml + "<TR VALIGN=TOP>" lcHtml = lcHtml + "<TD WIDTH=100 " lcHtml = lcHtml + "<FONT Class=HeaderSection>" + tcName + ", " + tcCompany + "</FONT>" lcHtml = lcHtml + "<TD WIDTH=80>" lcHtml = lcHtml + "<A HREF=mailto:" + Trim(tcEmail) + _ " Class=Blue Title='Send an email to this person'>" + _ "<IMG SRC=/Images/Email.gif WIDTH=32 HEIGHT=16 BORDER=0>" lcHtml = lcHtml + " " lcHtml = lcHtml + "<A HREF=" + tcUrl + " Class=Blue Title='Go to this Web site'>" + _ "<IMG SRC=/Images/URL.gif WIDTH=32 HEIGHT=16 BORDER=0></A>" lcHtml = lcHtml + "<TR>" lcHtml = lcHtml + "<TD COLSPAN=2 Class=Normal12>" lcHtml = lcHtml + tcBio lcHtml = lcHtml + "</TABLE>" lcHtml = lcHtml + "</TABLE>" End If lcHtml = lcHtml + "<P>" Return lcHtml End Function End Class
' Web service and environment initialization ' ... ' Get the upcoming user group meetings loData = loUT.GetUserGroupSpeakerDataSet(DateAdd("d", lnDaysBack * -1, Date.Today), _ DateAdd("d", lnDaysNext, Date.Today), lnUserGroupID) ' Get it into a view Dim loView As DataView loView = loData.Tables("Temp").DefaultView ' Display the list of speakers lcHtml = "" For lnCompteur = 0 To loView.Count - 1 Step lnCompteur + 1 lcHtml = lcHtml + _Function.AddSpeaker( _ loView(lnCompteur).Row("ID"), _ loView(lnCompteur).Row("Picture"), _ loView(lnCompteur).Row("FirstName") + " " + loView(lnCompteur).Row("LastName"), _ loView(lnCompteur).Row("Bio"), _ loView(lnCompteur).Row("Company"), _ loView(lnCompteur).Row("Email"), _ loView(lnCompteur).Row("Url")) lcHtml = lcHtml + "<P>" Next SpeakerVFP.Text = lcHtml
The evolution
What started as a simple request to have their meeting data to be feeded from the Universal Thread evolved into additional services such as having their board of directors page to be layed out the same as any speaker is bound to. The only requirement was to make sure any member of the board of directors has their biography up to date on the Universal Thread.
Montreal is a bilingual city. So, the need to offer the site in English and French has always been a requirement. In order to provide full flexibility within that data, the Universal Thread applied some enhancements in order to offer the biography in foreign languages as well. So, when applicable, the biography can be entered in foreign fields as well. That means when someone switches languages on their Web site, within that board of directors page, they will get the biography in the related languages as well. The same is true for the speakers page for those who have multiple versions of their biography in their profile.
Usually, only one version of the biography is used. But, in this case, where the user group is providing content in French and English, this ability becomes quite useful. It could also be used on DevTeach site where the list of speakers presenting at the conference is also provided in French and English. On the Universal Thread, we have that need on the Universal Thread Magazine site where English, Spanish and Portuguese are supported. So, within the magazine team, we try to have all biographies translated in those three languages.
While at it, we decided to feed the user group Web site by providing them new methods for other needs such as the picture archive. As many user group as uploading their pictures to the Universal Thread, it is now easy to provide such a content as well. Within the same approach, the MFUG Web site now has a picture archive where the pictures are coming in from the Universal Thread so is the picture title and date of meeting.
The request to get all the necessary information to display the picture is really simple:
' Get the upcoming user group meetings loData = loUT.GetUserGroupPictureDataSet(lnUserGroupID)
To provide other content of interest for their members, we added new items in the menu for the list of Visual FoxPro news and the list of Visual FoxPro conferences.
The advantages
What is great about that is the site is practically maintained standalone. Basically, once the data is entered on the Universal Thread, the user group Web site is automatically updated. Which means, the content of the main page, for example, is reflecting the new submitted data for the upcoming meetings. Only a few maintenance is necessary for customized content such as the list of specific user group news of interest for their members.
The following describes the list of advantages within such a context:
Considerations
This approach is nice but some considerations are present. Basically, it's like the Universal Thread Web site is now growing as per the MFUG traffic. This is in fact a reality. As, whenever a hit occurs on the MFUG Web site, one or multiple Web Service calls are being issued. That means, you need to analyze the expected traffic and see how that would influence your traffic load. As part of its line of services, the Universal Thread is doing a lot of work for the user group community. This is simply one add-on. User group Web sites also have, in most cases, a low traffic bandwidth. So, a few additional hits per day is not what would affect the Universal Thread server. But, if you would implement such infrastructure for your business, it might be good to consider it.
Of course, enhancements are possible. One of them is to use a Windows Service on the client server to query the Universal Thread server at specific interval and simply store the received data locally in XML file, for example. Then, the ASP.NET Web site application can simply include the XML string in the related pages. By that, you would be able to calculate exactly the amount of hits to your data server and you will also guarantee that when your client server is running, that the page will load in the browser. As you may know, sometimes, along the Internet route, failure may happen and that might result in a page not being able to load in the browser because the data server is unreachable.
The Windows Service is a great approach. It has been discussed on the Universal Thread in several threads in the .NET forum during the month of September and October. The UGMT Web Service page also includes a sample of a Windows Service which is querying the Universal Thread at specific interval to retrieve some data.
How to get it?
For user groups interested in getting the same, the Universal Thread provide some documentation and samples on putting it all together into its UGMT Web Service page.
References