Thursday 9 April 2009

Using ShouldSerialize for conditional omission of properties in the XmlSerializer

Wow

I've just found out about the undocumented ShouldSerialize technique for XmlSerializers in .NET. Possibly the most useful System.Xml discovery I've made for years. Basically, while it's not very OO (but when is codegen particularly OO?) it allows you to provide programattic control of when a property should be serialized to the XML result stream inside the generated XmlSerializer for that type.




Here's a link to the only MSDN article I've seen on the subject http://msdn.microsoft.com/en-us/library/53b8022e(VS.71).aspx

Why

You have to comply with an existing XML consumer (probably bespoke and third party) that isn't tolerant of fully schema-bound XML, and you want to use the built-in XmlSerializer in .NET. And why wouldn't you? it's extremely powerful and configurable - and there's a huge sense of satisfaction to be gained from getting it working with some esoteric consumer - done well, your code is clean, readable and functional - far better than messing around with StringBuilders and XmlWriters.




Now I've gotten quite good at manipulating the XmlSerializer over the years, so it tends to be my first port of call when I need to generate some XML for some reason. Who wouldn't want to replace gobs and gobs of String concatenation code with three lines of a call to a serializer. It's clear what you're doing and far far easier to maintain. The point being that XML is structured data, it's not just text, just because it can be represented that way.

The Scenario

I'm trying to automatically Serialize some CAML to send to SharePoint. Specifically a Query which looks something like this:

<Query xmlns="http://schemas.microsoft.com/sharepoint/soap/">
  <OrderBy>
    <FieldRef Name="Title" />
  </OrderBy>
</Query>

(1) Required XML from the serializer

I have a Query class which contains a List<FieldRef> called OrderBy. So far so good. Now, I also added a subclass of List<FieldRef> called GroupBy, adding the extra property Collapse, which you can see the schema requires. Now consider how I get the XML above from the serializer. My API looks something like this:


Query query = new Query();
query.OrderBy.Add(
new FieldRef ("Title"));

(2) API to build the XML in (1) above.

so I default the GroupBy and OrderBy properties to the Query to new instances, but since I haven't added a GroupBy, when I run I'll get this XML back.

<Query xmlns="http://schemas.microsoft.com/sharepoint/soap/">
  <OrderBy>
    <FieldRef Name="Title" />
  </OrderBy>
  <GroupBy />
</Query>

(3) XML generated by the serializer under normal circumstances.


See the extra GroupBy? That's no good. So how do we get rid of this empty element? Ok, I could annotate the GroupBy property with [DefaultValue(null)] and have the GroupBy property instantiate lazily. That's all well and good, and would work... until we want to remove a FieldRef from the list, leaving it empty. Same problem, the list isn't null although it's empty, so it serializes and we get the XML (3) above.




The problem is that DefaultValue doesn't allow conditional evaluation. What we need is something that behaves like the DefaultValueAttribute which tells the Serializer to skip the property when it has that value, but that does allow conditional evaluation. Of course we could performs some weird hacks, checking for an empty list in the property getter and returning null... that would keep the serializer happy, but it would break the API, forcing us to explicitly create the list from client code, and that's something easily forgotten leading to potential errors.



Enter ShouldSerialize. It turns out that creating a public boolean method called ShouldSerialize[PropertyName] in the serializable class tells the generated XmlSerializer to call that method to determine whether it should try to serialize the property or not. So, I create a method called ShouldSerializeGroupBy in my Query class, which checks for null or empty, returning false in that case, and BAM! tests pass, and I am happy. So my Query class now looks like this below.


[
XmlRoot("Query", Namespace = Namespaces.SharePointSoap),
XmlType("Query", Namespace = Namespaces.SharePointSoap),
Serializable
]
public class Query
{
private List orderBy = new List();
private GroupBy groupBy = new GroupBy();

[XmlArray("OrderBy")]
public List OrderBy
{
get { return orderBy; }
set { orderBy = value; }
}

public bool ShouldSerializeGroupBy()
{
return groupBy != null && groupBy.Count > 0;
}

[XmlArray("GroupBy"), DefaultValue(null)]
public GroupBy GroupBy
{
get { return groupBy; }
set { groupBy = value; }
}
}


Hope this helps you. It's saved me a lot of trouble. TTFN :)

Monday 26 February 2007

Getting Custom Actions to work in SharePoint Designer

I'm writing custom actions (i.e. Activities) for SharePoint Designer at the moment. It's lovely when it works, and incredibly frustrating when it doesn't. The problem is not so much that it's particularly hard once you've got the basics sorted, it's that it's so poorly documented, and so much of it is literally driving blind.


For instance, I've just spent another typical afternoon's debugging session trying to work out the following error in SPD.


The list of workflow actions on the server references an assembly that does not exist. Some actions will not be available. The assembly strong name is ... Contact your server administrator for more information.”


And I assure you, the assembly *does* exist. This is all made even more frustrating by the fact that I'm working against a development install of SharePoint remotely, and TDD just isn't an option here yet. I mean it's awkward enough for local ASP.NET pages, let alone ones that have to run remotely. Then factor in Windows Workflow, which is a horror to run in nUnit. Well, K. Scott Allen has been trying, but it's far from ideal.



So The last few hours have been characterised by making a small change, deploying and GACing assemblies, possibly, recopying .actions files, iisresetting, restarting SPD, screaming, then doing the same thing again. Over. And over. Again.



Basically I'd added a new Activity to my assembly, but it was an extremely simple one, so I'd had the confidence to write it all out by hand. Then I started getting this error, seemingly out of the blue. I looked over and over the code, compared it with a working Activity (One that was still working on a deployed workflow despite SPD apparently thinking it didn't exist.), compared it with the existing SPD activities via Reflector.. naughty, I know ;oP



I finally came upon this post today from Jason Nadrowski, which gave an insight into the problem. God knows how he found all this out, but I'm ever so glad he did.



http://blogs.informationhub.com/jnadrowski/archive/2007/01/12/8128.aspx



Thank you Jason, I tried to post a comment to your blog to say thanks, but it wouldn't let me. So I'm blogging it instead.



Jason's basically pointed out that When SPD loads your custom actions file, it also asks the server to test your objects. If any fail, SPD reports this error. And the server doesn't log the detail or the truth of the fact anywhere. So I don't know who this error message is aimed at. It's too technical for typical end users, it asks them to ask their administrator for more info, except he won't be able to give it, and it's a blatant lie if targetted at developers.



So anyway, armed with this new knowledge, I dug around. And I found my problem. Purely by speculation too. This may be obvious to the few of you who have been playing with XAML since the first Avalon CTP, if it is indeed a DependencyObject constraint, but it was sure news to me.



DependencyProperties need to be named [public property name]Property or they fail to work in SharePoint. so,


public static DependencyProperty EmailProperty =
  DependencyProperty.Register(
    "Email",
    typeof(string),
    typeof(MyActivity)
  );

public string Email {
  get {
    return base.GetValue(MyActivity.EmailProperty) as string;
  }
  ...
}


will work fine, but, freakishly, and not a little infuriatingly,


public static DependencyProperty _depPropEmail =
  DependencyProperty.Register(
    "Email",
    typeof(string),
    typeof(MyActivity)
  );

public string Email {
  get {
    return base.GetValue(MyActivity._depPropEmail) as string;
  }
...
}


will fail with the "assembly does not exist" error. And this is RTM! I do hope they patch it.

Tuesday 20 February 2007

Writing office documents over HTTPS

I love computers. Every job you do, every bit of code you write, no matter how simple or complex, you learn something new somewhere along the way.

And today was no exception. I had to deploy an update to our company's extranet. it was a fairly thorny one because the database schema had changed enough to make it an issue, well, the whole application had changed enough to make it an issue. Anyway, one of these new features was customer reporting. Nothing too exciting, you pick a date range, runa a SQL query and spit out the results. But we also provide a nice pretty formatted export to Excel using an in-house developed SpreadsheetML library, and write it out to the response.

The usual stuff, I'm sure we've all been there:


response.ContentType = "application/vnd.ms-excel";
response.AddHeader("Content-Disposition", "attachment; filename=" + filename );


Then, when I deployed it to the live site (at midnight!) it wouldn't work. Strange. It's exactly the same code. It's writing text to the response (XML) and setting a content type. That's it really, so why wouldn't it work?

Luckily, I found This KB article.
Turns out that Office documents don't download over HTTPS because, Quote:

In order for Internet Explorer to open documents in Office (or any out-of-process, ActiveX document server), Internet Explorer must save the file to the local cache directory and ask the associated application to load the file by using IPersistFile::Load. If the file is not stored to disk, this operation fails. When Internet Explorer communicates with a secure Web site through SSL, Internet Explorer enforces any no-cache request. If the header or headers are present, Internet Explorer does not cache the file. Consequently, Office cannot open the file.

Well, blow me down!

So, I added this line before doing anything else, and bingo!


response.ClearHeaders();


I mean, it figures and all, but I'd really rather not have found this out at half past 12 in the morning, instead of trying to get some sleep! I bet I'm the last person in the web world to find this out too. Bah.

Tuesday 23 January 2007

Solving The Vista Radeon Code 43 problem

If, like me, you have been a member of "The Code 43 Club", and various versions of Vista betas, and now RTM simply refuse to load the video drivers, you know how frustrating it can be. I have a 128MB Radeon 9800 Pro, not a kick-ass video card or anything nowadays, but certainly more than capable of pulling off anything Aero wanted to do. It can run FarCry at 1280x1024 with high detail, AA off admittedly, but no problems there. So there simply has to be a way to get it working. It's on the supported list for goodness sake!

There have been countless attempts to work around the issue, and believe me, I've tried them. You can install XP drivers (ATI or Omega) in XP Compatibility mode, and that goes some way towards addressing it - Vista will then load your drivers, and you'll get some hardware acceleration. But you won't get glass, since the DWM needs DirectX 10 drivers, and XP doesn't have any. As far as I understand, it never will either.

I tried the ATI Vista Beta drivers, the RTM drivers, fiddling with inf files and device IDs therein, trawling through the setupapi, setupact and setuperr logs in %systemroot%, flashing the BIOS on my card, who knows what, and still no glass. Still Code 43 in device manager.

Then there was a suggestion it was the motherboard, I had a MSI 746F Ultra at the time (yeah,yeah, check out granddad with his Socket A), which sports a SiS 746 chipset. Never had any problems with it, but it had to be worth a go looking around for GART drivers or whatever they're called. Nothing.

So I went and bought the cheapest new mainboard I could find that supported AGP, and bingo, it worked straight away! £40 for the board, £30 for a 512MB stick of DDR, has to be the cheapest new PC I've ever bought ;).

Since I built the pc for around the £300 mark 18 months ago I don't think £70 is a bad investment to make it "Vista Ready"

And that's why I don't touch macs.

Wednesday 17 January 2007

Copying folder contents with BCOPY through WebDAV


I'm working on an Exchange project at the moment, and have chosen to use WebDAV for the operations required. One of the subsidiary requirements is to copy contacts from one folder on a mailbox to another, then synchronise those contacts with a public folder on the Exchange server too. I discovered the WebDAV BCOPY method, which allows you to copy a whole load of items from one folder to another. Perfect! It works like this.

I make a WebDAV SEARCH request to the exchange server like so


<?xml version="1.0" ?>
<D:searchrequest xmlns:D="DAV:">
  <D:sql>
    select "DAV:href","DAV:contentclass"
    FROM SCOPE ('SHALLOW TRAVERSAL OF "/exchange/<mailbox>/Contacts"')
    WHERE "DAV:ishidden" = false
  </D:sql>
</D:searchrequest>


it returns a result set like this:


<?xml version="1.0" encoding="utf-16"?>
<a:multistatus xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" xmlns:c="xml:" xmlns:a="DAV:">
  <a:response>
    <a:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</a:href>
        <a:propstat>
          <a:status>HTTP/1.1 200 OK</a:status>
          <a:prop>
            <a:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</a:href>
            <a:contentclass>urn:content-classes:person</a:contentclass>
          </a:prop>
        </a:propstat>
      </a:response>
      <a:response>
        <a:href>...


and so on.

then you iterate through your <a:response>s, extract the hrefs and build this request


<?xml version="1.0" ?>
<D:copy xmlns:D="DAV:">
  <D:target>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</D:href>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/JudahBauer.EML</D:href>
    ... etc
  </D:target>
</D:copy>


add an Http header called Destination, with the value of the destination folder, then fire it off as a BCOPY command. Bingo, all your specified contacts have been copied, with changes from the source overwriting the destination object.

It's a bit long winded though. Not to mention the hassle of removing old entries. Anyway, it worked. So I kept experimenting, and tried adding a subfolder to the items collection to see what would happen if I tried to copy it. And whaddyaknow, it only bloomin created the destination folder, and all the contacts in it!

!!BRAIN WAVE!!

That means the entire operation can be condensed into this:


<?xml version="1.0" ?>
<D:copy xmlns:D="DAV:">
  <D:target>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts</D:href>
    <D:dest>http://<exchsrvr>/exchange/<mailbox>/ContactsCopy</D:dest>
  </D:target>
</D:copy>


fired off at the mailbox root. And it cascades item deletes and everything!

Laaaaahvley!

Tuesday 16 January 2007

Get with the program


Blogs.. I've never wanted one, it all seems too.... narcissistic. But it keeps coming back to haunt me that if you're not doing it, well you weren't there... Had one a while back, server crashed.. all seemed like a divine message - The world really doesn't want to hear my thoughts. But you know how it is, sometimes you just gotta tell people something.

So I gotta "Get with the program" again.

Just so you know - in case it wasn't clear, I'm yet another Microsoft coder. But I do bigger picture stuff too. You can come back here for whatever tidbits I've found out today. Hell you can even subscribe if you want, but you'd be a glutton for punishment if you do. I rant a lot, I talk crap a lot, and I'm frequently wrong (even if I won't admit it). But then again that's your choice. Don't say I didn't warn you.

So, with all that out of the way, let me get around to sharing something I just found out. Which is why I set this thing up in the first place.