Showing posts with label XML. Show all posts
Showing posts with label XML. Show all posts

Wednesday, 19 January 2011

Centralising global TFS alerts/notifications

TFS email alerts are critical to keeping your team informed about changes in the TFS projects they care about. At the very least, most users probably want to know when they've been assigned a work item.

Although Visual Studio 2010 includes the Alerts Explorer (Team –> Alerts Explorer) for signing yourself up for alerts, there's no obvious way to do the same thing for a group of users. Adding custom business logic into the mix really means you'll need to find a better alternative.

Behold! My latest Codeplex project is live, I call it TFS Global Alerts:

http://tfsalerts.codeplex.com/

When we last faced this problem with Visual Studio Team System 2008, we landed on a solution from vertigo.com--I'd link to the original blog post but it's been removed from the site. Moving forward a few years, it was recently time to upgrade our TFS 2008 install to TFS 2010 and the question naturally arose about what we do with alerts: continue using the Vertigo web service or not?

The custom logic originally added to the Vertigo solution was flailing a bit due to the kludge of time so we decided to refactor the current solution to better meet our needs and keep everything working with TFS 2010. I've uploaded a generalised version of the end result for your alerting enjoyment as an example of handling TFS events in a web service (you may want to follow the links below for alternatives based on Windows services and event listeners).

The Solution

TFS Global Alerts is, at its core, a web service containing a single public method:

[WebMethod]
public void Notify (string eventXml)

Notify() accepts a single parameter, a string containing XML detailing the change that occurred and some information about the work item itself. The Notify() method signature changed slightly from TFS 2008: it previously included a second string parameter, tfsIdentityXml. I removed this parameter to get things wired up successfully with TFS 2010.

Most elements in the eventXml look a bit like this—note the OldValue and NewValue child elements:

<Field>
<Name>State</Name>
<ReferenceName>System.State</ReferenceName>
<OldValue>Active</OldValue>
<NewValue>Resolved</NewValue>
</Field>

With the Notify() method called and most of the data you need at hand, sending an email to the relevant parties is then quite simple. TFS Global Alerts does some additional work to exclude the person who made the change from receiving a notification about that change (presumably they know what they've just done!) and avoid sending notifications about code check-ins linked to a work item.

The only real complexity in this code is retrieving an email address for each user from Active Directory. In our production environment I found TFS listed users by display name however in development, with TFS running as a service account in the dev domain, users were returned as domain\username. In production, if a user had multiple accounts (some of our admins), their name was returned as "First Last (domain\username)".

In terms of deployment, I simply deploy the web service to the web server hosting TFS itself and TFS web access.

The notification system hangs off TFS' own event subscription tool, bissubscribe.exe (you'll find it at C:\Program Files\Microsoft Team Foundation Server 2010\Tools\bissubscribe.exe). TFS raises events for all sorts of different happenings and bissubscribe.exe allows you to subscribe an email address or SOAP web service to handle those events.

Since I had some problems creating a server-level subscription, here's the command I use to create a project collection subscription (which captures all events in our case):

bissubscribe /eventType WorkItemChangedEvent /address http://localhost:8080/{vdir_path}/Alerts/WorkItemChanged.asmx /collection http://localhost:8080/tfs/{project_collection_name}

Result:

TF50001:  Created or found an existing subscription. The subscription ID is 36

Debugging

TFS 2010 only sends alerts every two minutes by default and events subscribed to via bissubscribe.exe adhere to the same policy. This can be a bit annoying when you're developing so you may want to dial down the batch wait time. I've included a Powershell script in the solution to configure this but otherwise, check out chrisid's post on the subject.

For additional help debugging bissubscribe, Grant Holliday has some useful tips.

Finally, don't forget to actually configure your TFS server to send email!

Resources

If you found this post helpful, please support my advertisers.

Monday, 7 June 2010

Australian Weather Data (RSS…)

Despite my best efforts, I've been disappointed in my (repeated) attempts over the last twelve months to locate a free Australian weather feed I can consume via RSS and present as I wish. The Bureau of Meteorology is operating in the dark ages when it comes to the distribution of its authoritative weather data, Yahoo! weather—although available in a nicely structured XML document—is not available for commercial use, and all other feeds available at the time of writing send down HTML-formatted data with very limited rights for modifying the HTML itself.

Naturally, this is all very frustrating, especially when your client's intranet site needs weather for three specific towns in the middle of the outback or you're trying to build a redistributable weather widget (SharePoint web part anyone?).

Having given up on a free RSS feed, I returned to the BoM site as they do provide weather data via FTP (they also provide registered users access to additional services for a subscription fee). The terms are a bit unclear about distribution and attribution so I'll leave them up to you to interpret. This post presents my approach to accessing and parsing the basic (free) weather data file. A bit more work is involved than simply pointing a nice XDocument at an RSS feed URL and LINQ-ing away but hey, lower-level stuff is always fun and educational ;-)

By the way, this code will eventually make its way into SharePoint web part; if you're interesting in licensing this web part for use on one of your own sites, please drop me a line – info@mediawhole.com.

Access

The BoM provides weather data in the form of .dat, .txt, .html, and other file types via anonymous FTP. The .dat file is what we're after and for this example, I'll be using the IDA00006.dat file (Western Australia).

.dat files contain forecast information by town with details separated by a hash (#) character. The first line in the file describes the file format so if you get lost is a sea of hashes, start there. The Precis Forecast Products page provides formal documentation, of a sort.

Since the .dat file is just a text file we'll be using the WebClient class to manage access to the FTP location and open a readable stream over the .dat file URL:

WebClient client = new WebClient();

using (Stream data = client.OpenRead("ftp://ftp2.bom.gov.au/anon/gen/fwo/IDA00006.dat";))
{
}

Parsing

With access to the file contents, we can now parse it for easier access. To do so, we'll use a StreamReader to enumerate the file, tokenize each line, and store the results in a generic list:

using (StreamReader reader = new StreamReader(data))
{
    List<List<string>> weatherData = new List<List<string>>();
    string currentLine;

    while ((currentLine = reader.ReadLine()) != null)
    {
        string[] tokens = currentLine.Split('#');
        weatherData.Add(tokens.ToList<string>());
    }
}

It's worth pointing out the nested list structure I'm using here in place of a dedicated collection type. Each line in the file represents data for a single town; individual data elements within each line are separated by a hash character. To accommodate this, I tokenise each data element using the String.Split() method after reading the next line and store those elements in a nested list; each outer list item therefore contains data for a single town.

If that sounds at all complicated, here's a picture of the end result:

NestedListsThe parsed data is finally cached (not shown) for subsequent access. Notably, the BoM doesn't specify any limits on access attempts per day or an effective TTL value as do many RSS feeds. There's nothing stopping you from retrieving this data with every page load but your site will likely slow down and you'll also be guilty of gumming up the intertubes. You may also run the risk of being blocked from accessing the site.

Data Extraction

The final step in this process is locate and extract the weather data you're after (most likely for a specific town). I use the List<T>.Find() method, supplying a predicate that matches on town name, to accomplish this:

List<string> town = weatherData.Find(currentTown => currentTown[WeatherIndex.Location].ToLowerInvariant() == "perth");

Final Touches

The free data doesn't provide any form of condition code so associating a forecast (e.g. "Mostly sunny") with an icon is going to be tricky. I'm hopeful the BoM uses standard phrases that can be matched (or at least partially matched as a fallback (e.g. contains "sun") against known terms for hookup to generic icons but this could become a maintenance problem.

But that's it, weather!

Sunday, 4 April 2010

Flatten XML files with the XML Flattener

Have you ever needed to “flatten” a pretty-printed XML file for pasting into a web service test page? XmlSpy and the like will happily display ugly XML in a nicely-indented format but the version I have doesn’t un-pretty-print (or flatten) XML for me.

Custom tool to the rescue! Announcing my very first codeplex project and the XML Flattener!

This first alpha release is a simple WinForms apps with zero error handling and very limited capabilities allowing you paste in an XML document, flatten it, and copy out the flattened string. The hard work is accomplished by—wait for it—leveraging the default formatting of the XmlDocument’s OuterXml property (which isn’t formatted, unlike other XML-related classes which do return formatted documents).

XML FlattenerFYI I built this app years ago when I was doing a lot of web service integration work so it’s a fairly naive tool built primarily for testing web services. It’s original title was The XML Bastardiser but I’ve attempted to make the name a bit more relevant with this first public release ;-)

Enjoy!

Thursday, 23 October 2008

sgen.exe /compiler:/keyfile Doesn't Allow Spaces

While implementing a bit of .NET code that will ultimately live in SQL Server 2005, we came across a few minor problems with the auto-generated XmlSerializers.dll and, in particular, signing it with a key file. The assembly calls out to a web service, hence the need for the XmlSerializers.dll.

Normally you can tell Visual Studio to build this assembly for you at compile time: Project properties -> Build Tab, Generate serialization assembly = On | off (note this is set per configuration, eg. Debug, Release). SQL Server had a few problems around versioning with the generated assembly, however, so we decided to add a post-build event and use sgen.exe explicitly to manage this at build time. 

Soon after we also decided the main assembly and XmlSerializers assembly would need to be signed, as it's also being called from another bit of code in MOSS; we naturally figured adding the /compiler:/keyfile flags would take care of this.

The path to our .snk file has spaces, however, and the keyfile flag doesn't seem to like spaces. Initially I thought we would simply xcopy the .snk file to the c: drive and then reference it directly from there but that's dirty. 

In the end, there is a way to escape the quotes in the string: 

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\sgen.exe" /compiler:"\"/keyfile:$(ProjectDir)MyKeyFile.snk"\" /force "$(TargetPath)"



Custom-Built Microsoft Office SharePoint Server 2007 Branded Sites and Webpart Development - info@mediawole.com

Sunday, 25 March 2007

XmlSerializer and minOccurs="0" collections: that pesky xxxSpecified magic

I'm attempting to decorate the properties in a class so when it's serialised by the XmlSerializer, it adheres to a schema that looks, in part, like this:

<xs:element minoccurs="0" name="Locations">
<?xml:namespace prefix = xs /><xs:complextype>
<xs:sequence>
<xs:element name="Location" maxoccurs="unbounded" type="xs:string">
</xs:sequence>
</xs:complextype>

Basically, I've got a class that contains a Locations collections; if the collection has one or more items, it should be serialized and if it has zero items, it shouldn't be serialized and, most importantly, the Locations element should NOT appear in the serialized document.

Running the property below through the XmlSerializer works fine if the collection is populated but results in an empty element if the collection is empty.

So this:

[System.Xml.Serialization.XmlArrayItemAttribute ("Location")]
public List Locations
{
get
{
return SearchAttributes.Locations;
}
}

populated at runtime with zero items results in this:

<locations>

And schema validation fails.

I've looked at various attributes trying to find something that will optionally serialize an element or collection but nothing jumps out.

XSD.exe produces the following property from the schema but by itself does nothing to help out here.

[System.Xml.Serialization.XmlArrayAttribute(Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
[System.Xml.Serialization.XmlArrayItemAttribute("Location", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=false)]
public string[] Locations {
get {
return this.locationsField;
}
set {
this.locationsField = value;
}
}

Another XSD example produced an xxxSpecified property for a minOccurs="0" element (not a collection) and setting it false does remove the element from the output; I can't figure out why this works though! Sticking it into my own class does some sort of magic though:


private bool locationsSpecified;

[System.Xml.Serialization.XmlIgnoreAttribute ()]
public bool LocationsSpecified
{
get
{
return this.locationsSpecified;
}
set
{
this.locationsSpecified = value;
}
}


The LocationsSpecified property is simply set by the caller and voila, Locations disappears if empty.