Gravatars and monsters

by Matt 30. July 2007 08:11

The Gravatar is a fiendishly simple idea; register an image with your email address, and that image is displayed in the comments of (supporting) blog software. A nice easy way to maintain a visual cue on who people are, and who's talking when in the conversation.

But if you haven't registered a gravatar, what then? Normally, you get a blank image, but you can supply a fallback redirect url with your gravatar link.

Phil Haack and Jon Galloway created an HttpHandler implementation of Don Park's Identicon idea - a way to generate abstract geometric shapes based on an IP address. And of course, used it as the fallback to the gravatar image.

But if that's just a little too abstract for you (and since it's based on IP address, not permanent), you could always use a MonsterID, which is the same sort of idea, but based on the same MD5 hash of your email adress that gravatar uses, and displaying, well, monsters. (More examples and details are on the announcement page than the project page. And here's an alternate template, but I can't find a download link.)

Fractals, anyone?

So that's comments sorted - what about trackbacks? Well, you could use a WebSnapr thumbnail, but that might be just too small to be any use, so how about a favatar? (That's an avatar taken from the favicon of the website sending the trackback/pingback.) There doesn't appear to be a .net implementation, but it shouldn't be too hard.

Now all you need is a server side cache of some sort so that you're not continually regenerating all these images (and proper etag handling for client side caching)...

Tags:

Service Trace Viewer

by Matt 27. July 2007 04:58

Audience participation time! Download this file, it contains a copy of the Service Trace Viewer program and some demo files (as far as I can see the viewer is only available as part of the Windows SDK, which makes it a little hard to get a hold of).

The demo files are a set of verbose logs from a simple WCF client calling an equally simple WCF service to add 2 numbers and return the result.

Two important links for this one - again, MSDN has the docs in two different places. Here's the doc for the .exe, which provides a metric truckload of information on how to use the viewer. Then this page is a bit more of a tutorial style page, with lots of pictures. I'd strongly suggest reading them both.

Not that you will, so let's crack on. Fire up the viewer and load the service.stvproj file to load all the service .svclog files. You'll see something like this:

image

The left hand pane is a flattened list of all the activities in all the trace files. Select an item here, and on the top right, you see all the events in that activity, including transfers to and from other activities. Bottom right is the details of a given event, including details such as timestamp, callstack, process details and even the actual WCF message.

If you double click an activity on the left pane ("Listen at http://localhost:8000/CalculateService" is a good candidate for exploring), you'll get taken to the graph view.

image

Here it is, all things collapsed. It starts on the left with the root activity, and then transfers through all activities (without showing details) until it reaches the Listen activity. The two yellow boxes are events, one of which is the activity boundary, the other is that the client is now actually starting to listen for messages. Select any of these events, and the associated event is highlighted in the two window panes on the right, showing more details.

You can see that this "listen" activity transfers back to the calling (and eventually root) activity, but still continues. When a message does arrive, it is logged in the "listen" activity. We can switch the display mode to Thread (top of the graph view) to see this a bit more explicitly.

image

Each of the collapsed boxes in the new thread are activities that represent messages arriving. Expanding this first gives us a "receive bytes" activity, with another nested activity - "Processing message 1". One of the events in this nested activity is a "received message" event. Select this and on the bottom right we see a new tab called "Message" which displays the actual SOAP message received (minus any private information - WCF automatically strips known personal information). And we see that this is a request for a security token (WCF is secure out of the box - I would have to create more config to disable security).

Expanding the next activity event gives a whole heap of events and we now start to wish we hadn't enabled verbose logging.

image

Now what we have here is 4 activities - listen, receive bytes, process message, and the huge process action "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue", which is the SOAP action for issuing a security token via WS-Trust. We can see that processing the Issue action is continuing while several other activities are occurring (the listen activity in the first column has several other activities that are timestamped during the lifetime of the Issue activity). That's because there are several messages exchanged to bootstrap the secure conversation. If you're feeling brave, open up all the activities (leave the fourth one for the minute) and look at the messages. You'll soon find you're looking at encrypted data.

This is a complex example to show as a demo, but it's worth seeing what a real-world trace would look like, how much data is in the verbose traces and especially important to see that activities are not stacks and can happen in parallel.

Incidentally, if there's too much of the verbose logging, you can filter it out, either with the options at the top of the graph view, or by changing the level in the search filter at the top of the main window - this gets rid of a lot of noise.

It's probably worth collapsing all the Listen sub-activities and just look at the fourth one. This is finally the call to the Add service method. Expand its sub-activities, and you'll end up with 5 activities in all - listen, process message 4, receive bytes, process action "http://tempuri.org/ICalculator/Add" and execute service "WcfService.ICalculator.Add". This very last activity is the actual code for the service method. And in it, you should see a simple trace output I added with the following code:

TraceSource ts = new TraceSource("WcfService.TraceSource", SourceLevels.Information);
ts.TraceInformation("Adding {0} and {1} = {2}", a, b, a + b);

(No reference to activities here - the trace automatically picks up the current activity.)

So that's activities and correlation. Let's have a quick look at propagation. Close that project and open all.stvproj. This contains both the service and the client logs, again at full logging levels.

In the Activity list on the left, find the activity called Process action 'http://tempuri.org/ICalculator/Add'. This is the client calling the Add web service operation. Double click the activity, and you'll see a graph of what the client goes through before calling into the server. And it all nicely joins up to the server trace logs, so you now see a full end to end call.

image

And that's a quick run around with Service Trace Viewer. It's certainly powerful, but I don't think it's the most intuitive program - there's a lot of data, and lot of axes to be looking along, which can be a bit overwhelming. The best thing to do is just play around - select things, double click things, filter things and see what happens.

I think its biggest failing is the difficulty of finding that one trace you're looking for. It's awkward not being able to select the root activity - you have to jump in at the right start activity (clearly displaying the root activity as a graph is just asking for trouble - everything starts off in the root activity, so you're going to get a huge graph). I think searching is going to work best here - but again, you need to know what you're searching for. If you're just looking at errors, then the tool helps; any activity that contains an exception gets highlighted.

But hey - nice graphs!

Tags:

Tracing nirvana. Activities, identity, transference and propagation of.

by Matt 27. July 2007 04:52

Oh yes. More tracing. Push through the pain barrier. Things are actually starting to get interesting.

Tracing is only useful if you do something with the output. Clearly, we're going to get a heap load of information; we need to be able to manipulate it. Currently, we can use logging level or source to filter and route it.

This isn't enough. It makes it easy to see what's being logged for a particular component, or what errors we're getting, but that's about it. We're missing the ability to be able to trace a call end-to-end.

And that's where Activities come in.

We wanted to build something like this into the telemetry framework for Trafalgar, but couldn't decide on requirements that weren't hideously complex. We should have just waited a few years - Microsoft have built it for us.

The System.Diagnostics.Trace class has a static property called CorrelationManager. This class provides a place to store a thread local activity id.

An activity is simply a unit of work, and the id is used to correlate events logged within this unit. You can start and stop an activity, suspend and resume it. More interestingly, you can transfer from one activity to another, and propagate an activity between processes and machines via WCF or remoting.

This gives us everything we need to do end-to-end tracing. It's easiest to look at an example in the Service Trace Viewer, and that'll be the next (final) post on tracing.

It's worth pointing out that activity ids are not a stack. You don't push a new activity, do some work and then pop it - you always transfer between activities. This makes activities much more flexible in their lifetime management, meaning that activities are not limited to the same physical flow as your code. You can suspend and resume a long running activity or perform asynchronous activities (by transferring back to an existing activity and moving the current activity to another thread).

While this is all part of System.Diagnostics, only the XmlWriterTraceListener actually outputs the activity id. This listener creates xml fragments following a schema called End To End Tracing - a schema WCFs Service Trace Viewer understands.

It's a bit odd that XmlWriterTraceListener is part of .net2, but the first thing that actually makes use of it is .net3! In fact, the tracing documentation on msdn comes in two parts - the .net 2 pages cover the basics of TraceSource and TraceListener, but the only place that end to end tracing is covered is in the .net 3 docs.

(As it happens, the WCF docs on configuring tracing also provide a very useful list of what gets logged at what levels - very handy for deciding what you should log in your own code.)

Now then, I promised pictures...

Tags:

Configuring a TraceSource

by Matt 27. July 2007 04:50

A common pattern in log4net is to create an instance of a logger in a class, passing in the name of the class. This logger can then be setup via config files and violin, you've got tracing that you can turn on and off at the class level. Rather handy.

TraceSource allows exactly the same thing. Simply pass a name into the constructor and define the parameters in the app.config. But the common idiom is to not build a TraceSource per class, but group them at a more logical (as in "not-physical", rather than Vulcan) level, usually named after a namespace, but sometimes also a class or other logical identifier. This gives you finer control of the logging of an entire feature set.

Why am I telling you this?

Because generally, the .net framework doesn't have much logging of what it's doing. .net 3 has tons, and it's all done through TraceSources. This page on msdn details how to turn on tracing for WCF. Here are the TraceSources defined:

  • System.ServiceModel: Logs all stages of WCF processing, whenever configuration is read, a message is processed in transport, security processing, a message is dispatched in user code, etc.
  • System.ServiceModel.MessageLogging: Logs all messages that flow through the system.
  • System.IdentityModel
  • System.ServiceModel.Activation
  • System.IO.Log: Logging for the .NET Framework interface to the Common Log File System (CLFS).
  • System.Runtime.Serialization: Logs when objects are read or written.
  • CardSpace

Quite a few sources. But looking at the recommended tracing levels, or using the Service Configuration Editor, you might be surprised to see more than just "System.ServiceModel" or "System.ServiceModel.MessageLogging". I'm certainly regretting not finding the "CardSpace" source sooner.

WPF gets in on the act too, with it's own set of TraceSources:

  • System.Windows.Media.Animation
  • System.Windows.Data: Databinding
  • System.Windows.DependencyProperty: Tracing for the dependency property system
  • System.Windows.Documents
  • System.Windows.Freezable: Tracing for objects that have been made immutable
  • System.Windows.Markup: All about XAML
  • System.Windows.NameScope: Tracing naming and the scopes used for name lookup and collisions
  • System.Windows.ResourceDictionary
  • System.Windows.RoutedEvent

The odd thing about WPF logging is that not only do you have to configure it in your app.config, you also need to enable a registry key. I don't know why the config isn't enough, I guess it's to do with performance.

Tags:

Why didn't someone tell me C# has typedef?

by Matt 19. July 2007 04:42

What do you do if you want a mapping between a string and a guid?

I reached for Dictionary<string, guid>. Job done. Strongly typed goodness.

But it's a bit ugly, isn't it? I need to use the generic parameters everywhere I want to use the type, so if I suddenly decide I want to use a structure instead of a guid, there's a lot of places to change.

If I want to abstract my code away from all of this implementation detail, I can create a derived class:

class SectionMapping : Dictionary<string, guid>
{
}

Which feels a bit klunky - empty classes are not the nicest of things.

So today's bright idea was to use a nice little trick with using:

using CategoryMapping = System.Collections.Generics.Dictionary<string, System.Guid>

And I can now use CategoryMapping wherever I had been using the Dictionary before.

The downside is that this is not a new type, like the SectionMapping class is - it's just an alias. In other words, I can use CategoryMapping and Dictionary<string, guid> interchangeably. If I create another "using" alias for, say, GroupMapping, then I can use an instance of CategoryMapping wherever I use GroupMapping. Contrast this with classes. If I define CategoryMapping and GroupMapping as empty classes that derive from Dictionary<string, guid>, they have the same interface, but the compiler views them as very different types.

Oh and it's only scoped to the C# file in which it's declared.

Tags:

Rediscovering the BCL - tracing

by Matt 17. July 2007 03:15

Back in the mists of .net 1.x, if you wanted to put any kind of logging (or - if you're posh - telemetry) in your application, you generally avoided the Base Class Libraries, and for good reason. What was there was basic, and not massively flexible.

You had System.Diagnostics.Trace and System.Diagnostics.Debug. They would write out to any and all TraceListeners defined in config (of which there could be Default, EventLog and TextWriter). There was no great difference between the two - the output wasn't categorised as trace or debug, and they both went to the same set of listeners.

Categories were a bit rubbish - they simply got prepended to the text that was output. No filtering and no routing of categorised output to a specific listener. The way to do conditional output was to check with a named Switch before calling Write. The output level of the Switch (or rather its derived classes TraceSwitch and BooleanSwitch) would get set in config. This lead to code like:

if(traceSwith.TraceInfo)
    Trace.Write("Hello");

which leads to the WriteIf family of methods:

Trace.WriteIf(traceSwitch.TraceInfo,"Hello");

(One kinda nifty feature was that each of the Write methods on these classes are marked with the ConditionalAttribute, meaning Trace.Write would only work if you'd compiled with the TRACE constant defined, and Debug.Write requires the DEBUG constant. The nifty bit is that if you don't have the constants defined, the call to the method is optimised away. It's like surrounding the call to Debug.Write in #ifdef's. But it wouldn't optimise away the "if" when using switches. Hence WriteIf.)

All in, not too terrible, but not very practical, either. The switch idea is ok, but you decide in code (not config) if it's going to be all or nothing (BooleanSwitch) or based on trace level (TraceSwitch). It's a bit restricting that you can only output text, and any extra information you want, you have to get yourself. You're mainly limited by outputting everything to all listeners.

The normal reaction to this was to reach for a 3rd party solution, such as log4net, or Enterprise Library. Both of these provide categorisation, routing to different listeners and finer control of what gets output.

It's what we did at work. We used Enterprise Library (and Enterprise Instrumentation Framework before that, I believe) and it hurt, so we wrote a wrapper API for it, and that also hurt, so we wrote a new wrapper for it, and then migrated to EntLib 2, which changed their API, and we wrote a new wrapper for that.

And now I'm looking at WCF, and especially the Service Trace Viewer, and I need to understand how that works, which means a dive back into .net 2's tracing features.

Guess what? They've pretty much fixed it.

You've still got Trace and Debug, and they still function basically the same. But you don't use those in new code. You use TraceSource instead. You create a named instance of one of these, and call TraceEvent, TraceInformation or TraceData on it. Each TraceSource can have its own set of TraceListeners or use any from a pool of shared listeners. Giving it a name effectively gives you categories (a common naming convention is to use your namespace), and lets you define routing in config. A TraceSource also has a SourceSwitch to provide blanket filtering (by trace level) for all events from a TraceSource. Each listener can have a TraceFilter, which also filters events - useful for shared listeners where the TraceSource isn't configured the same. And a TraceListener can now automatically output a bunch of useful information along with the text, including callstack, timestamp and process and thread id. Arbitrary data can be logged with TraceData.

There are even a few more listeners: Default, EventLog, TextWriter, Console, DelimitedList, WebPage (nice!) and the most interesting one, XmlWriterTraceListener.

(And there's a StopWatch, too.)

Indeed it looks like the only reason to go with a 3rd party solution now is for more out of the box listeners. Log4net ships with a ton of them. Enterprise Library (which was rearchitected to use .net 2's logging) adds a few more (MSMQ, Wmi, email and so on). But if you don't want to take the dependency, it's not hard to write a custom TraceListener (or just use standard .net 2 APIs and the EntLib listeners).

But the killer app for .net 2 logging? That's where XmlWriterTraceListener, WCF's Service Trace Viewer, the CorrelationManager's ActivityId's and End-to-End Tracing come in. The next post will have pretty pictures...

PS. A few more useful links:

  1. MSDN's tracing docs (part 1)
  2. More info on .net 1.x's poor logging
  3. Tracing in asp.net + playing nice with System.Diagnostics.Trace
  4. BCL Team Blog - A Tracing Primer - Part I
  5. BCL Team Blog - A Tracing Primer - Part II (A)
  6. BCL Team Blog - A Tracing Primer - Part II (B)
  7. BCL Team Blog - A Tracing Primer - Part II (C)

Tags:

QuickTime stealing file associations

by Matt 5. July 2007 07:40

Didn't we already get tired of this, like 5 years ago?

Got QuickTime? Got IE? Right. Go here: http://www.w3.org/Graphics/PNG/inline-alpha.html. You should see something like:

image

Ok. Now go here: http://www.w3.org/Graphics/PNG/alphatest.png. It's the URL of the image shown above. Chances are, you'll probably see:

image

Doesn't quite look the same, does it? We appear to have lost some transparency. Let's try right clicking on it:

image

Ah. QuickTime.

Come to think of it, I did just upgrade iTunes the other day. In fact, I remember very deliberately clicking to not associate file types. And indeed, if I look closely in the registry, my .png file associations are untouched (and double clicking a file opens into Windows Photo Gallery). If I look in Vista's rather nice new file association window, I see that it doesn't allow me to reassign my .png associations back to IE, or even to QuickTime! According to this UI, QuickTime shouldn't be handling .png files.

And yet, it is. It's handling the image/png MIME type - by installing an Internet Explorer plugin. (It'd be nice if the Vista file association UI could in future be extended to include MIME associations.)

A quick spelunk through the HKCR\MIME\Database\Content Type tree of the database reveals quite a few instances of {4063BE15-3B08-470D-A0D5-B37161CFFD69} - the QuickTime plugin. I certainly didn't ask for these to be associated.

Fortunately, there's a quick fix - fire up the QuickTime control panel applet, get to the Browser tab and hit "MIME settings". You can now choose which MIME types you want the QuickTime plugin to handle (there are a bunch that Windows can't handle out of the box).

But you know what? I'd already done this last time I updated iTunes. And still it stole them back again. And what's more, going into the control panel app, it played innocent and claimed that these MIME types weren't associated with QuickTime! A quick toggle of a setting on and off and an apply later, and all is well with the world again:

image

So what's worse? That Apple don't honour MIME types as well as they do file associations or that they ignore your settings and install something that doesn't work as well as the native implementation?

Tags:

Mounting vhd files

by Matt 4. July 2007 17:39

I meant to post about this last month, but it slipped by. Microsoft have released (deep breath) Virtual Server 2005 R2 Service Pack 1. It's got a whole heap of features, and probably loads of improvements over previous versions - I can't say I really know.

All I'm interested in is the rather nifty ability it gives you to mount vhd files as drives. Yep, you can now browse and modify the contents of your virtual machine hard drives without booting up the vm itself. How cool is that?

Oh, and of course, vhd is the file format used by Vista's backup, so I guess you can browse those too (and retrieve data quickly and easily?)

And did I mention that it automatically creates a temporary undo disk for you?

It's a all a bit prosaic in that it's just a command line app, and one that's not very well documented, either. Trust this post over the usage instructions. And you've got to love the fact that it comes with an API, even if that API doesn't provide all the support of the command line app (doesn't allow you to specify deleting or committing the undo disk changes; doesn't provide support for the ALL parameter to unplug all virtual drives), or that it won't support an even more useful shell context menu handler without having to write dodgy and bound-to-be-error-prone state management code (i.e. it doesn't tell you which drive is running what vhd files). So it's handy there's a poor man's version ready for you, eh?

So what if you don't want to install Virtual Server? Perhaps you have Virtual PC already installed, or you're just not interested in having it installed? Well, just kick off the install, select customise and disable everything except vhdmount. Or if you're feeling particularly command line oriented, try msiexec /i "Virtual Server 2005 Install.msi" /qn ADDLOCAL=VHDMount.

Just something else to add to your box of tricks.

PS. Bonus tip - turn on tracing.

Tags:

XmlResolver reading from resources

by Matt 3. July 2007 06:02

Right. Putting my money where my mouth is.

After a rather lengthy discourse on what on earth the XmlResolver is all about, I outlined how I would implement XmlResolver to resolve external files from resources (based on, but quite different to a very useful example from Scott Willeke).

I actually implemented it pretty much as I described it, and it works like a charm, and is, I think, rather more obvious in what it's doing. Here goes:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;

namespace SticklebackPlastic.Xml {

    internal sealed class XmlResourceResolver : XmlUrlResolver {

        private readonly Dictionary<string, Resource> _publicIdResources = new Dictionary<string, Resource>();
        private readonly Dictionary<string, Assembly> _assemblyMapping = new Dictionary<string, Assembly>();

        private class Resource {
            private readonly Assembly ResourceAssembly;
            private readonly string _resourceNameAsPath;

            public Resource(Assembly resourceAssembly, string resourceNameAsPath) {
                ResourceAssembly = resourceAssembly;
                _resourceNameAsPath = resourceNameAsPath;
            }

            public Uri AbsoluteUri {
                get {
                    UriBuilder builder = new UriBuilder();
                    builder.Scheme = ResourceScheme;
                    builder.Host = ResourceAssembly.GetName().Name.ToLower();
                    builder.Path = _resourceNameAsPath;

                    return builder.Uri;
                }
            }
        }

        internal void AddPublicIdMapping(string publicId, Assembly resourceAssembly, string resourceNamespace, string resourceName) {

            // Treat the resource namespace as a path - replace the "." separators with "/", and append the resource name
            string resourceNameAsPath = resourceNamespace.Replace(".", "/") + "/" + resourceName;

            Resource resource = new Resource(resourceAssembly, resourceNameAsPath);
            _publicIdResources.Add(publicId, resource);
            string assemblyName = resourceAssembly.GetName().Name.ToLower();
            if (!_assemblyMapping.ContainsKey(assemblyName)) {
                _assemblyMapping.Add(assemblyName, resourceAssembly);
            }
        }

        public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) {

            if (absoluteUri.Scheme == ResourceScheme) {
                Assembly resourceAssembly = _assemblyMapping[absoluteUri.Host];
                string resourceName = absoluteUri.AbsolutePath.Substring(1).Replace("/", ".");
                Stream stream = resourceAssembly.GetManifestResourceStream(resourceName);
                return stream;
            }

            return base.GetEntity(absoluteUri, role, ofObjectToReturn);
        }

        public override Uri ResolveUri(Uri baseUri, string relativeUri) {

            // Is this a DocType Public Id?
            if (baseUri == null) {
                Resource resource;
                if (_publicIdResources.TryGetValue(relativeUri, out resource)) {
                    return resource.AbsoluteUri;
                }
            }

            return base.ResolveUri(baseUri, relativeUri);
        }

        private const string ResourceScheme = "resource";
    }
}

The first significant change is to derive from XmlUrlResolver, instead of the abstract XmlResolver. This allows us to defer to an instance of XmlResolver.GetEntity that can download an xml file from a given URL (file or http). Now we can just concentrate on handling our own URI scheme.

Other than that, it's essentially as I described in my last post - I only recognise DocType Public Id's, and convert them to my own resource:// URI scheme during ResolveUri, with the host being the assembly containing the resource, and the path being the namespace of the resource. Any external references will get resolved against this URI, so must live in the same resource namespace, with the correct name. GetEntity simply pulls the resource out as a stream, or passes the request to the base class.

The limitation of this class is that it only recognises DocType Public Ids. If I wanted to simply replace a http based external reference, I'd need to tweak the class to capture that information and intercept it in ResolveUri. It wouldn't be difficult, and is left as an exercise to the reader.

Finally, I've got two little factory classes to create the XmlResolver and the XmlParserContext:

using System.Xml;

namespace SticklebackPlastic.Xml {

    internal static class XhtmlParserContextFactory {

        internal static XmlParserContext CreateStrict() {
            XmlNameTable nameTable = new NameTable();
            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(nameTable);
            XmlParserContext context = new XmlParserContext(nameTable, namespaceManager, null, XmlSpace.None);

            context.DocTypeName = "html";
            context.PublicId = "-//W3C//DTD XHTML 1.0 Strict//EN";
            context.SystemId = "xhtml1-strict.dtd";
            return context;
        }
    }
}

and:

using System.Reflection;
using System.Xml;

namespace SticklebackPlastic.Xml {

    internal static class XhtmlResolverFactory {

        internal static XmlResolver Create() {
            XmlResourceResolver resolver = new XmlResourceResolver();

            Assembly assembly = typeof (XhtmlResolverFactory).Assembly;
            string resourceNamespace = typeof (XhtmlResolverFactory).Namespace;

            resolver.AddPublicIdMapping("-//W3C//DTD XHTML 1.0 Strict//EN", assembly, resourceNamespace, XhtmlStrictDtd);
            resolver.AddPublicIdMapping("-//W3C//DTD XHTML 1.0 Transitional//EN", assembly, resourceNamespace, XhtmlTransitionalDtd);
            resolver.AddPublicIdMapping("-//W3C//DTD XHTML 1.0 Frameset//EN", assembly, resourceNamespace, XhtmlFramesetDtd);

            return resolver;
        }

        private const string XhtmlStrictDtd = "xhtml1-strict.dtd";
        private const string XhtmlTransitionalDtd = "xhtml1-transitional.dtd";
        private const string XhtmlFramesetDtd = "xhtml1-frameset.dtd";
    }
}

Forgive the lack of xml docs, this post is long enough without it...

Tags:

Month List

RecentComments

Comment RSS