tag:blogger.com,1999:blog-71622982008-07-26T13:43:16.509+01:00Kent BoogaartKent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comBlogger97125tag:blogger.com,1999:blog-7162298.post-11173791879907787452008-07-22T16:01:00.001+01:002008-07-24T19:46:38.877+01:00Paris<p>Dirty. Expensive. Confusing. Those are words I would use to describe Paris.</p> <p>Perhaps we were unlucky, but the neighborhood we stayed in (north-east of the city, near <a href="http://en.wikipedia.org/wiki/Gare_de_Nord">Gare du Nord</a> station) was far from idyllic. The streets were paved in rubbish and dog crap, and we witnessed plenty of Parisians adding their own bodily fluids into the soup. Charming.</p> <p>Eating out was nice enough, but really took its toll on the wallet. One place charged us €6.50 per beer! Of course, they didn’t tell us that until we’d finished drinking. And when we questioned the price, the manager was at the ready with a menu in hand. I suspect it was the specially-printed “tourist” menu.</p> <p>As for getting around, it wasn’t <em>too</em> bad but was quite frustrating at times due to the strange layout of the city and the lack of good signs. There’s appears to be nothing consistent or logical about the geometry of the streets. It’s really kind of random.</p> <p>It’s not all bad though. Central Paris was amazing. <a href="http://en.wikipedia.org/wiki/Notre_Dame_de_Paris">Notre Dame</a> and <a href="http://en.wikipedia.org/wiki/Mus%C3%A9e_du_Louvre">The Louvre</a> were a sight to behold (apart from the eyesore that is The <a href="http://en.wikipedia.org/wiki/Louvre_Pyramid">Louvre Pyramid</a>). The <a href="http://en.wikipedia.org/wiki/Eiffel_Tower">Eiffel Tower</a> also looked great, but we didn’t get up close.</p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SIjM9kEGEzI/AAAAAAAAAvw/f2cOaiypUoU/s1600-h/P7170023%5B4%5D.jpg"><img title="P7170023" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P7170023" src="http://lh4.ggpht.com/kent.boogaart/SIjM-H6DFEI/AAAAAAAAAv0/qEXxWWyJr3k/P7170023_thumb%5B2%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SIjM_Y2j4ZI/AAAAAAAAAv4/9k-2G3Bsk-M/s1600-h/P7170025%5B4%5D.jpg"><img title="P7170025" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P7170025" src="http://lh4.ggpht.com/kent.boogaart/SIjNASNeuuI/AAAAAAAAAv8/J5jqMPUK4w0/P7170025_thumb%5B2%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SIjNBDvfd3I/AAAAAAAAAwA/nzHTikanwmA/s1600-h/P7170036%5B4%5D.jpg"><img title="P7170036" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P7170036" src="http://lh4.ggpht.com/kent.boogaart/SIjNBsYJFlI/AAAAAAAAAwE/Gu93GmcVMvI/P7170036_thumb%5B2%5D.jpg?imgmax=800" width="260" border="0" /></a></p> <p> <a href="http://lh3.ggpht.com/kent.boogaart/SIjNCpoUyZI/AAAAAAAAAwI/k7SABI3rw0s/s1600-h/P7170039%5B4%5D.jpg"><img title="P7170039" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P7170039" src="http://lh4.ggpht.com/kent.boogaart/SIjNDHtvVrI/AAAAAAAAAwM/Gp50OjvhSpc/P7170039_thumb%5B2%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p>The second day saw us at <a href="http://en.wikipedia.org/wiki/Disneyland_paris">Disneyland</a> where rides and signatures are free, assuming your time and energy are worthless. Seriously – well over an hour to get Cinderella’s signature. By the time I reached her, she had changed shift and I got Snow White instead. I was <em>heartbroken</em> ;)</p> <p>Tempany loved it all, though, which is the important thing.</p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SIjND5qsTXI/AAAAAAAAAwQ/NMMIk398nz8/s1600-h/IMG_0676%5B5%5D.jpg"><img title="IMG_0676" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="IMG_0676" src="http://lh5.ggpht.com/kent.boogaart/SIjNEamba_I/AAAAAAAAAwU/7rAqcxAfBRw/IMG_0676_thumb%5B3%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SIjNGYWqtAI/AAAAAAAAAwY/naJdJ5QT7RM/s1600-h/IMG_0678%5B4%5D.jpg"><img title="IMG_0678" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="IMG_0678" src="http://lh6.ggpht.com/kent.boogaart/SIjNG8MeVMI/AAAAAAAAAwc/FZToCySsW04/IMG_0678_thumb%5B2%5D.jpg?imgmax=800" width="260" border="0" /></a></p> <p> <a href="http://lh6.ggpht.com/kent.boogaart/SIjNIKvN0rI/AAAAAAAAAwg/CAUod3E2kIk/s1600-h/IMG_0680%5B4%5D.jpg"><img title="IMG_0680" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="IMG_0680" src="http://lh5.ggpht.com/kent.boogaart/SIjNIgwGnyI/AAAAAAAAAwk/ZbPwu_Fpr98/IMG_0680_thumb%5B2%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p>The next day we went shopping. We were told to check out a local shopping center, and check it out we did. It was pretty good, and I picked up some shirts (€35 each, in case you were wondering). However, I was disappointed not to see more Asterix and Obelix merchandise.</p> <p>The final day was a write-off, as we pretty much just hung around waiting for our train. Incidentally, <a href="http://www.eurostar.com/dynamic/index.jsp">Eurostar</a> was great. Far less hassle than flying, and the difference in travel time is negligible.</p> <p>I’ll probably go to France again one day, if only to see the Asterix and Obelix theme park.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-23659470334834871082008-06-30T14:38:00.001+01:002008-06-30T19:22:17.483+01:00Hawkeye<p><img title="image" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="100" alt="image" src="http://lh6.ggpht.com/kent.boogaart/SGkkWKM843I/AAAAAAAAAu0/LCsm-JiWJ94/image%5B8%5D.png?imgmax=800" width="100" align="right" border="0" /></p> <p> Finally, it looks as though <a href="http://www.cardiff.ac.uk/socsi/newsandevents/news/hawkeye.html">Hawkeye will be outed</a>. It was always telling to me that suddenly we weren’t shown video replays of close calls anymore, only the Hawkeye simulation. I suspect if we were shown side-by-side replays all along then the technology would have proved to be inaccurate to the general public a lot sooner. Of course, it’s probably still more accurate than the human eye.</p> <p></p> <p>On the subject of tennis, we went down to Wimbledon last Friday. There was a nice five hour wait to get into general admission. Five hours in line with a three year old to see maybe the last hour’s play? <em>Sure!</em></p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-89976687058975217752008-06-18T09:32:00.001+01:002008-07-26T13:39:35.957+01:00MAF Gymnastics: Service Provider<p><a href="http://www.users.on.net/~kentcb/MAF/ServiceProvider.zip">Download the Solution</a></p> <p>If you’ve ever worked on a composite application, you’ll know that services are their backbone. Services allow modules in the application to collaborate in both state and behavior. Moreover, services can be swapped out easily, since the modules work against an interface and are (hopefully) ignorant of the implementation behind the interface.</p> <p>This post will address how to implement a service ecosystem using MAF in a composite application. In particular, how we can do this whilst keeping the host agnostic of the services it is hosting, and providing version tolerance for the services. Whilst I focus on composite applications, the techniques here can also be applied in non-composite applications.</p> <h3><strong>Requirements</strong></h3> <h4><strong>Agnostic Host</strong></h4> <p>One of the challenges of implementing a service provider in MAF is ensuring that the host is capable of hosting <em>any</em> service, not just those services it knows about at compile-time. Without this ability, it’s not really a composite application and does not permit the kind of collaboration between add-ins that such an application requires.</p> <p>Of course, the types related to the services must not be loaded into the host’s <code>AppDomain</code>. The services need to be isolated just like any other add-in.</p> <h4><strong>Version Tolerance</strong></h4> <p>Consider how composite applications are often written in the real world. You have a bunch of autonomous teams, each team producing and/or consuming services. Some teams consume services <em>provided by other teams</em>.</p> <p>Then there’s the shell (aka “host”), which is responsible for hosting these modules and allowing them to collaborate. The shell is often produced by another team altogether to those producing the modules (aka “add-ins”).</p> <p>Now think about what happens when one of the service-providing teams decides they need to modify their service interface. They can potentially break all the modules that depend on that service. In other words, the consumers of that service are at the mercy of the service provider.</p> <p>MAF pipelines already provide a nice solution to this problem between the host and add-ins, but wouldn’t it be nice if the service provider could version their service over time, providing backwards compatibility as desired?</p> <h3><strong>Solution</strong></h3> <h4><strong>Add-in Types</strong></h4> <p>The contracts define two types of add-ins: an application add-in and a service add-in.</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="287" alt="image" src="http://lh6.ggpht.com/kent.boogaart/SGIAoGHXUqI/AAAAAAAAAuY/YRy4ukItfog/image5.png?imgmax=800" width="589" border="0" /> </p> <p>At this point, the difference between a service add-in and an application add-in is minimal. A service add-in provides an ID by which it will be resolved by consuming add-ins.</p> <p>Both add-in contracts have a common base interface (<code>IAddInContract</code>) from which they inherit an <code>Initialize()</code> method. Notice how this method returns an instance of <code>AddInInitResult</code>. This is crucial to the host being able to track which add-ins are hosted in which <code>AppDomain</code>s.</p> <p>The add-in views do not have this added complexity:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="261" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SGIAodp4JoI/AAAAAAAAAuc/86a1vHLTToE/image14.png?imgmax=800" width="396" border="0" /> </p> <p>This is good, because the add-in developer need not worry about returning anything from their <code>Initialize()</code> implementation. Instead, this is handled by the adapters, as we’ll see later.</p> <p>The host views mirror the contracts very closely, since the host <em>does </em>need to know and track this extra initialization information:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="261" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SGIAojxML7I/AAAAAAAAAug/rRqmexV2PHs/image17.png?imgmax=800" width="587" border="0" /> </p> <h4><strong>Add-in Activation and Tracking</strong></h4> <p>When the host starts up, it first finds and activates service add-ins and then application add-ins. In both cases, it remembers the contract object for the add-in (that is, the <code>MarshalByRefObject</code> implementing the add-in). For services, it also maps the service ID back to the <code>AddInToken</code> so that it can obtain the contract implementation given only the service ID. This is important, of course, and you’ll see why soon.</p> <p>Note that the tracking information is only actually necessary to support services, but the host code tracks application add-ins too, since that made the logic simpler.</p> <h4><strong>The Host</strong></h4> <p>It’s time to look at the pipeline components for the host. Firstly, the contract:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="254" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SGIAqYSsouI/AAAAAAAAAuk/aru-cAIsnOI/image29.png?imgmax=800" width="612" border="0" /> </p> <p>We’re getting closer and closer to the <a href="http://dictionary.reference.com/browse/nitty%20gritty">nitty-gritty</a> now (I can’t believe that’s in the dictionary). The host contract has two methods of importance on it: <code>GetPipelineDetails()</code> and <code>GetServiceDetails()</code>.</p> <p>The former is quite simple: it just retrieves some basic information about the MAF pipeline. This information is needed by the add-in side adapters to support services. We’ll look at this further below.</p> <p>The latter is also straightforward: it retrieves the service contract for a given service ID. Recall from above that when we activate services we track some information about that service. This method just allows the caller to retrieve that information.</p> <p>The host view of the host is a mirror of the contract, so I won’t bother talking about it here. The add-in view of the host, on the other hand, is quite different:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="156" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SGIAqmIHR-I/AAAAAAAAAuo/N9QFnwWPats/image34.png?imgmax=800" width="228" border="0" /> </p> <p>As you can see, add-ins get a simpler view of the host. There is a single method that they can use to resolve a service by ID. There is no nonsense about getting pipeline information or what have you. The service consumer doesn’t care about that - she just wants a service.</p> <p>I made this method generic just to save a little casting in the add-ins, but in reality I haven’t decided how I’d best like to compose this interface. I’m considering having the ID automatically determined by the interface type the service implements, so add-ins don’t even need to provide an ID. However, I haven’t spiked this yet so I don’t know if it will play out exactly as I hope. Anyway, I’ll be looking at that soon for a real project.</p> <p><strong>UPDATE</strong>: It works great! In my “real” project services do not need to explicitly provide an ID. Instead, the adapters infer it based on the view type the service implements. In addition, services are not explicitly requested from the host. Instead, the adapters determine the services an add-in requires by examining its <code>Initialize()</code> method and injecting any dependent services automatically. This results in a really nice, low-friction add-in development experience.</p> <h4><strong>The Adapters</strong></h4> <p>Based on the interfaces we just saw, you might assume that there’s some magic happening in the adapters to close the gap, so to speak. And you’d be right.</p> <p>What we need to do is connect up the service consumer add-in to the service add-in, which already exists in its own <code>AppDomain</code>. Indeed, we may need to connect up multiple consumers to the service. Those consumers might be application add-ins or even other service add-ins (the sample only demonstrates the former, but the latter should work too assuming that the services are activated in the correct order).</p> <p>Consider the following diagram:</p> <p><img title="Service Provider" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="392" alt="Service Provider" src="http://lh3.ggpht.com/kent.boogaart/SGIAsGS4V-I/AAAAAAAAAus/3qlWFnTDW8o/ServiceProvider6.png?imgmax=800" width="640" border="0" /> </p> <p>The black lines represent the “primary” pipeline between the host and the add-ins, be they regular application add-ins or service add-ins. In the diagram we have a single service add-in (up the top) and two consumers of that service. The two consumers are built against different versions of the service (V1 and V2) but they both need to consume whatever version of the service is loaded (assuming the appropriate pipeline adapters can be found).</p> <p>The service’s interface (<code>ISomeService</code>) extends the core service interface provided by the host (<code>IService</code>). Actually, this extension occurs in both the contracts and the views. As such, the service pipeline components depend on the pipeline components provided by the host. For the purposes of the primary pipeline, only the implementation of <code>IService</code> is used (that is, <code>IServiceContract</code> is used for communication over the black pipelines).</p> <p>The blue lines represent the “secondary” pipeline between the service consumers and the service. It is this pipeline that provides marshalling specific to the service implementation (<code>ISomeService</code>).</p> <p>And it is here where we need to employ some trickery to get the job done. First though, thanks to the anonymous commenter who pointed out an easier way to do this.</p> <p>Here’s how it works. The add-in side contract to view adapter for the <code>IHost</code> interface implements the <code>ResolveService<T>()</code> method we saw above. In order to do this, it needs to construct a pipeline between the caller (ie. the consuming add-in) and the service itself.</p> <p>The host’s <code>AppDomain</code> is impartial to this whole process – it merely provides some information the adapter needs to set up the pipeline. Once the adapter has this information, it uses MAF’s <code>ContractAdapter</code> type to adapt between the add-in’s view of the service and the existing service instance (ie. create the pipeline represented by the blue lines above). Specifically, the <code>ContractToViewAdapter()</code> method is used because we’re taking an existing contract (the service instance) and adapting it to the view.</p> <p>Since the <code>ContractToViewAdapter()</code> method is generic and we don’t know the type until runtime, we need to invoke it reflectively. It might be nicer if MAF included non-generic overloads for these methods, but the fact that they’re public means that we do not need elevated privileges to invoke them via reflection. Thus, your add-in can still be hosted in a low-trust <code>AppDomain</code>.</p> <p>Of course, once this pipeline is constructed we don’t need to do so again for the current <code>AppDomain</code> (unless a different view of the same service is used from within the same <code>AppDomain</code>, and the code caters for that case too). Therefore, we cache this information. Subsequent calls to <code>ResolveService<T>()</code> won’t even leave the caller’s <code>AppDomain</code>, which I think is pretty cool. Of course, calling into the service itself will marshal to the service’s <code>AppDomain</code> as expected.</p> <h3><strong>Limitations</strong></h3> <p>There are a couple of things you need to be aware of with respect to this technique. First and foremost, I mentioned above that MAF can only deal with one pipeline at a time. Further to this, it can only resolve one assembly from each pipeline folder. For example, all your add-in side adapters need to be in a single assembly inside the <em>[Pipeline]\AddInSideAdapters </em>folder. If your add-in side adapters assembly depends on another assembly, that assembly must be in the GAC in order for MAF to resolve it. MAF will not try to resolve that assembly from the same pipeline directory from which your add-in side adapters were loaded.</p> <p>The problem with this is that the service pipelines depend on the host pipeline. As a simple example of this, the <code>ISomeServiceContract</code> included in the download extends the host’s <code>IServiceContract</code>. The upshot is that the host’s pipeline must be in the GAC.</p> <p>In fact, this GAC registration requirement spreads virally throughout your pipeline projects. If you look in the sample you’ll see that most pipeline assemblies are registered in the GAC when you build. As such, you’ll need local admin since <code>gacutil</code> requires this. Unfortunately, I don’t see a way around this given MAF’s current architecture.</p> <p>It’s important to point out that installing the pipeline components in the GAC <em>is almost certainly what you want to do for deployments</em>. Otherwise these assemblies cannot be shared between <code>AppDomain</code>s and performance suffers badly. It’s just a shame that it’s also required for development.</p> <p>Related to this issue is the possibility of name clashes in pipeline assemblies. You may have many teams working on services, and the pipeline assemblies for those services must reside in the same folder. Therefore, if one team chooses the same assembly name as another you will get a clash. This is pretty unlikely, but it’s something to keep in mind.</p> <p>Less a limitation and more a “gotcha”: notice how the service implementation (in the <em>Service </em>project) references the service add-in views project (<em>Service.AddInViews</em>) and the service consumer add-ins reference the service host views project (<em>Service.HostViews</em>). In MAF terminology, the consumers are “hosts” of the service. This terminology doesn’t quite fit here because the consumers aren’t explicitly activating the service. In a real application, I would probably rename the service add-in views and host views to “provider views” and “consumer views” accordingly.</p> <h3><strong>The Result</strong></h3> <p>If you download and execute the sample you’ll see this:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="195" alt="image" src="http://lh6.ggpht.com/kent.boogaart/SGIAtt3ddjI/AAAAAAAAAuw/-PBQi05nGkE/image40.png?imgmax=800" width="800" border="0" /> </p> <p>As unexciting as this looks at first glance, it’s actually really, really cool. <em>Trust me</em>. What we’re seeing here is that there are four <code>AppDomain</code>s:</p> <ul> <li>One for the host </li> <li>One for the service </li> <li>One for the first service consumer </li> <li>One for the second service consumer </li> </ul> <p>The host and its pipeline know nothing about the specific service at compile-time. That is, it has no knowledge of <code>ISomeService</code>, only <code>IService</code>. And yet, it is hosting this service. That’s a check for an agnostic host.</p> <p>The first service consumer is executing against version 2 of the service. It sets a value on the service to 26 and outputs the results of calling the service’s <code>GetTimestamp()</code> method. When it calls this method, it passes in a custom format string of “ddd MMM yyyy”.</p> <p>The second service consumer is executing against version 1 of the service. It gets the value of 26 set by the first consumer and outputs it. Then it calls the service’s <code>GetTimestamp()</code> method and outputs that. But what’s cool is that version 1 of the service did not allow a custom format string to be passed into this method! The adapters for the service (see the <em>Service.HostSideAdapters_V1toV2 </em>project) are automatically using a default format string of “dd/MM/yyyy”. This proves that the correct pipeline components have been selected and used to join the service consumers to the service – that’s a check for version tolerance.</p> <p>We have achieved everything we set out to achieve here. The host is agnostic of the services it will be hosting at runtime and none of the service types leak into the host. Moreover, the pipeline between service consumers and the service itself has all the intelligence you’d expect of a MAF pipeline. It selects the correct components to ensure that the service consumer can interact with the service, even if the consumer was built against an older version of the service.</p> <h3><strong>Conclusion</strong></h3> <p>Composite applications need a facility to provide and share services. An effective shell for a composite application needs to be agnostic of the services it is hosting, otherwise developers leveraging the shell are hamstrung by its intrinsic assumptions about the services it will host.</p> <p>MAF’s concept of pipelines to handle version tolerance are equally applicable to services as they are to the shell itself. That’s simply because of the relationship between the provider of the deliverables and its consumers. As a shell or service developer, there is some uncertainty as to how many teams are leveraging your work, and how long they need to migrate to newer versions of your deliverables. Pipelines allow you to provide backwards compatibility so that consumers are not forced to update their code bases straight away.</p> <p>The solution presented in this post solves both these problems. The shell knows nothing about the exact services it will be hosting at runtime. In addition, it uses a pipeline provided by the service developer to connect the service consumers to the service.</p> <p>This was a particularly difficult topic to articulate, and I’m not confident that I did so well. If I failed to convey this information adequately, feel free to let me know in the comments and I’ll do my best to rectify the situation.</p> <p><a href="http://www.users.on.net/~kentcb/MAF/ServiceProvider.zip">Download the Solution</a></p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-62881184498168935882008-06-11T21:40:00.001+01:002008-06-12T22:05:21.462+01:00MAF Gymnastics: Event Hub<p><a href="http://www.users.on.net/~kentcb/MAF/EventHub.zip">Download the Solution</a></p> <p>The first problem I will attempt to address in this series is that of providing an <a href="http://martinfowler.com/eaaDev/EventAggregator.html">event aggregator</a> service (which I call an event hub, just to save on typing). That is, how can we allow add-ins to subscribe to, and publish events on a many-to-many event bus that is managed and implemented by the host?</p> <p>When using MAF, there are a number of problems that must be solved in order to facilitate such a service. For one, how can we present a decent API to the add-in developer? The lack of generics support in MAF makes this difficult (see my <a href="http://kentb.blogspot.com/2008/06/maf-gymnastics-skeletal-solution.html">previous post</a> for details), and limits the overall effectiveness of the solution.</p> <p>Next up, how can we ensure that event subject types (the CLR types that contain event payloads) do not leak into the host? If two add-ins communicate with a custom subject type, that type should not be loaded into the host's <code>AppDomain</code>. It should only be loaded into the <code>AppDomain</code> of those add-ins that care about that particular event.</p> <p>With that in mind, how can we allow the host to use the event hub in much the same way as add-ins? That is, how can we design the host views such that the host can remain agnostic of subject types where necessary, but can publish and subscribe on the event hub where desired?</p> <p>Finally, we'll discuss how the event hub service is exposed to add-ins. That is, how does an add-in actually obtain an instance of the event hub service.</p> <p><strong>The API</strong></p> <p>The contracts for the event hub service looks like this:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="166" alt="image" src="http://lh3.ggpht.com/kent.boogaart/SFGOt3Z2pNI/AAAAAAAAAuE/43R4J7Uv1bM/image%5B39%5D.png?imgmax=800" width="778" border="0" /> </p> <p>Importantly, you can see that there is nothing in these APIs that will cause types to leak from the add-in <code>AppDomain</code>s to the host (or vice-versa). A <code>string</code> is used to identify subjects as opposed to a CLR type or <code>RuntimeTypeHandle</code>, both of which will result in errors because the corresponding subject type may not be available to the host. Also, subjects themselves are passed around as a <code>byte</code> array rather than an <code>object</code> instance. That allows the host to remain completely unaware of the actual CLR types involved.</p> <p>The views for the event hub service look slightly different depending on whether you're an add-in or the host. For an add-in, the API looks like this:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="153" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SFGOvuU1_hI/AAAAAAAAAuI/Dvkr4cliz34/image%5B38%5D.png?imgmax=800" width="634" border="0" /> </p> <p>The <code>ISubscriber</code> interface is what event subscribers must implement. It has a single method that is called when a relevant event is published on the event bus. The <code>IEventHubService</code> interface allows events to be published, and subscribers to be registered and unregistered.</p> <p>A CLR type is used to scope events. That is, subscribers register their interest in events with a certain subject type. Subscribers will only receive events of types they have registered their interest in. In reality, you might want add-ins to just use a <code>string</code> to identify events. However, I wanted something that demonstrates how to differ your view from your contract.</p> <p>This API is fairly clean and simple, but suffers from a lack of generics. Ideally, we would be able to define a generic <code>ISubscriber<T></code> interface and have subscribers receive a strongly-typed subject argument. However, whilst the MAF pipeline does allow generic members in non-generic types, it does not allow generic types. That's because it must automatically hook up contracts and views with their corresponding adapters. If the contract or view is a generic type, MAF is not able to close the type (presumably because it does not have enough information to do so).</p> <p>The API for the host is a little more complicated than that for the add-in:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="474" alt="image" src="http://lh6.ggpht.com/kent.boogaart/SFGO0QIPVhI/AAAAAAAAAuM/tyw0TWp1l_A/image%5B40%5D.png?imgmax=800" width="572" border="0" /> </p> <p>The reason for the complication is that we want the event hub to be usable from the host in much the same way as it is from an add-in. To that end, we differentiate between two different subscribers: add-in subscribers and the host itself. There are basically two "modes" that the host-side works in: type-agnostic (add-in subscriber) and type-aware (host subscriber). The type-aware mode is not strictly necessary for the host to work with the event hub, but it sure makes it easier and consistent with the way the event hub works for add-ins.</p> <p>The finally part of the API I want to draw your attention to is the <code>IHost</code> interface:</p> <p><img title="image" style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="160" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SFGO2gQZdqI/AAAAAAAAAuQ/P-fKyxQWy1g/image%5B41%5D.png?imgmax=800" width="306" border="0" /> </p> <p>The <code>EventHubService</code> property on this interface is the means by which add-ins actually gain access to the service. In a later post I will discuss a more generalized and extensible way of exposing services to add-ins.</p> <p><strong>Adapters</strong></p> <p>The key to this solution is contained within the adapters. The adapters ensure that the host remains blissfully unaware of subject types and payloads. Consider this add-in code:</p> <pre class="code">host.EventHubService.Publish(<span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">CustomEventSubject</span>() { Message = <span style="color: rgb(163,21,21)">"Giggidy giggidy!"</span> });</pre>
<a href="http://11011.net/software/vspaste"></a><a href="http://11011.net/software/vspaste"></a>
<p>The add-in side view-to-contract adapter adapts this call such that the subject type (<code>CustomEventSubject</code>) is converted to a <code>string</code> used to identify the event, and the subject itself (the <code>CustomEventSubject</code> instance) becomes a <code>byte</code> array via standard .NET serialization. Neither parameter makes it unscathed into the host's <code>AppDomain</code>, which is fortunate because that would cause problems where the subject type was unknown to the host. The contract-to-view adapter does the opposite: it resolves a .NET <code>Type</code> instance based on the identifying <code>string</code>, and deserializes the <code>byte</code> array into an instance of the subject type.</p>
<p>If little alarm bells are going off inside your head right now after hearing about the automatic (de)serialization, it’s for good reason. I discuss why you should use the event hub service with care below.</p>
<p><strong>Host</strong></p>
<p>The host is largely untouched with respect to the <a href="http://kentb.blogspot.com/2008/06/maf-gymnastics-skeletal-solution.html">skeletal solution</a>. The one change is the use of qualification data to dictate the load order for add-ins. We need the subscriber add-in to load prior to the publisher add-in, otherwise the event publication will occur before there is a subscriber!</p>
<p>Each add-in has a <code>QualificationDataAttribute</code> applied to it to specify the load order. The host then uses the value in this attribute to order the add-ins prior to activation:</p>
<pre class="code"><span style="color: rgb(0,0,255)">var</span> orderedTokens = <span style="color: rgb(0,0,255)">from</span> token <span style="color: rgb(0,0,255)">in</span> tokens
<span style="color: rgb(0,0,255)">orderby</span> <span style="color: rgb(0,0,255)">int</span>.Parse(token.QualificationData[<span style="color: rgb(43,145,175)">AddInSegmentType</span>.AddIn][<span style="color: rgb(163,21,21)">"LoadOrder"</span>])
<span style="color: rgb(0,0,255)">select</span> token;</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>Note that this is just a simple fix for this specific scenario. A more real-world solution would be to allow one add-in to depend on another and order the activations accordingly. Perhaps I’ll address this in a later post.</p>
<p>When you execute the host, you should see something like this:</p>
<p><img title="image" style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="196" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SFGO5R1WA8I/AAAAAAAAAuU/zokjS88WbbU/image%5B37%5D.png?imgmax=800" width="997" border="0" /> </p>
<p>As you can see from the output, both the add-ins and the host are utilizing the event hub service. The add-ins are communicating with <code>string</code> subjects, and with a custom subject.</p>
<p><strong>Deployment</strong></p>
<p>Again, deployment hasn't really changed much compared to the skeletal solution. The one big difference is the <em>Subjects </em>project. This project contains a subject type that the add-ins use to communicate. Since both add-ins require access to this type, it must be referenced by each add-in and loaded into their <code>AppDomain</code>. Therefore, the build script for each add-in project copies this file to the add-in’s directory:</p>
<pre class="code"><span style="color: rgb(0,0,255)"><</span><span style="color: rgb(163,21,21)">Copy</span><span style="color: rgb(0,0,255)"> </span><span style="color: rgb(255,0,0)">SourceFiles</span><span style="color: rgb(0,0,255)">=</span>"<span style="color: rgb(0,0,255)">$(OutputPath)\PublisherAddIn.dll;$(OutputPath)\Subjects.dll</span>"<span style="color: rgb(0,0,255)"> </span><span style="color: rgb(255,0,0)">DestinationFolder</span><span style="color: rgb(0,0,255)">=</span>"<span style="color: rgb(0,0,255)">..\Host\$(OutputPath)\AddIns\PublisherAddIn</span>"<span style="color: rgb(0,0,255)"> /></span></pre>
<p><strong>Performance</strong></p>
<p>Notably missing from the APIs is a filtering mechanism. That is, a way for publishers or subscribers to filter their interest in a particular event. I excluded this for two reasons:</p>
<ol>
<li>It was too much effort for the purposes of this post, and would complicate things quite radically. </li>
<li>It would encourage cross <code>AppDomain</code> filtering, which is a potential performance issue. </li>
</ol>
<p>Any event published to the event hub service has the potential to visit many <code>AppDomain</code>s, so it should be used with care and only when necessary. If your add-in requires an event hub service, but it does not need to cross the <code>AppDomain</code> boundary (ie. it is internal to your add-in) you might instead consider something like <a href="http://kentb.blogspot.com/2008/03/event-hub.html">this</a>. To reiterate, the service discussed in this post should be used only when inter-add-in communication is required. Don’t use it for intra-add-in communications.</p>
<p><strong>A Word of Warning</strong></p>
<p>One thing you must be aware of before using this service is that it circumvents some of the measures MAF takes to protect you from versioning issues. Consider what would happen if you had a custom subject type that was consumed (subscribed to) by many add-ins. Suppose you wanted to alter that custom subject type and those alterations would break existing add-ins. You don’t have the luxury of using adapters to help out because the subject was never explicitly called out in the contracts. Instead, the best you can do is use built-in .NET serialization mechanisms (for example, <code><a href="http://msdn.microsoft.com/en-us/library/system.runtime.serialization.optionalfieldattribute.aspx">OptionalFieldAttribute</a></code>) to provide some compatibility, but this will be limited when compared to using a MAF pipeline.</p>
<p>Because of this, you want to be sure that any custom subject types you create are resilient to change. And when they must change, be sure to ensure that changes are backwards compatible (write some unit tests to verify). And if you simply cannot preserve compatibility, consider defining a new subject type altogether because the alternative is requiring every add-in be recompiled against the new version of the subject type.</p>
<p><strong>Conclusion</strong></p>
<p>An event hub service is imperative for applications where add-ins are required to communicate with each other in a loosely-coupled fashion. The solution I presented in this post allows you to achieve that even when add-ins are in completely separate <code>AppDomain</code>s (or even processes) and the host has no knowledge of the types being used to communicate between the add-ins.</p>
<p>Limitations of MAF prevent us from providing an ideal solution - one that leverages generics to provide a strongly-typed API. However, this solution is still very workable. Just be cautious when defining subject types - you don't want them to become a versioning headache, especially considering MAF is designed to help prevent such headaches.</p>
<p><a href="http://www.users.on.net/~kentcb/MAF/EventHub.zip">Download the Solution</a></p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-32685067348584543002008-06-08T12:55:00.001+01:002008-06-08T12:55:52.105+01:00MAF Gymnastics: Skeletal Solution<p>To kick off this series of posts, I've put together a <a href="http://www.users.on.net/~kentcb/MAF/SkeletonSolution.zip">skeletal solution</a> that will be used as a starting point for subsequent posts. MAF requires a lot of plumbing code and infrastructure to be in place before even the simplest example will work. As such, it is prudent for me to avoid duplicating much of that plumbing for each post.</p> <p>If you download the solution, you'll see that it is comprised of quite a few different projects. Those projects are partitioned into solution folders.</p> <p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="72" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SEvIqZWwbJI/AAAAAAAAAs8/YctnJ-G-0F4/image%5B4%5D.png?imgmax=800" width="173" border="0" /> </p> <p>Most of the projects are contained in the "Pipeline" solution folder. It is these projects that implement the pipeline that MAF uses to connect the add-in to the host. The "AddIns" solution folder contains any add-in projects (there is one sample add-in in the download). Finally, the "Host" project is a simple console host that activates any add-ins that it finds.</p> <p><strong>The Pipeline</strong></p> <p>The pipeline consists of a bunch of projects:</p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SEvIrI4_YeI/AAAAAAAAAtA/XbnmF7d8xas/s1600-h/image%5B13%5D.png"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="179" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SEvIrlMWYsI/AAAAAAAAAtE/oWSlZoxc7Xs/image_thumb%5B5%5D.png?imgmax=800" width="169" border="0" /></a>  </p> <p>The <em>Contracts </em>project defines the contracts that MAF will load into both sides of the isolation boundary. It consists of a few simple contracts: <code>IHostContract</code> , <code>IAddInContract</code> and <code>IApplicationAddInContract</code>.</p> <p><code>IHostContract</code> provides some properties that add-ins can invoke to get information about their host (name and version, to be specific). <code>IAddInContract</code> defines a single <code>Initialize()</code> method that add-ins implement. This method takes an instance of <code>IHostContract</code> as a parameter, so the add-in can invoke members against the host. <code>IApplicationAddInContract</code> extends <code>IAddInContract</code> but does not add any members. In later posts I will have more than one type of add-in. <code>IAddInContract</code> will serve as a base contract for add-ins.</p> <p>The <em>AddInViews</em> and <em>HostViews</em> projects define the view of the world for the add-in and host respectively. In this case, they mirror the contracts very tightly, but in later posts we'll see views that differ quite drastically from their corresponding contracts.</p> <p>The <em>AddInSideAdapters </em>and <em>HostSideAdapters</em> projects provide adapters that MAF uses to adapt views to contracts and vice-versa. The adapters allow the view to be ignorant of the contract and vice-versa, which is required for supporting version tolerance.</p> <p>Note that I have not used any code generation or tools to generate the pipeline. It is written entirely by hand. The reason I did not use <a href="http://www.codeplex.com/clraddins">Pipeline Builder</a> is because it is simply not mature enough. I ran into many bugs and limitations when I tried to use it, and it does not give enough control over the generated code.</p> <p>When I originally decided to ditch Pipeline Builder and write my pipeline by hand, I tried factoring out common adapter functionality into generic base classes. Unfortunately, MAF does not allow adapter classes to inherit from generic classes, even if the adapter itself is closed (I consider this an awful bug and have <a href="http://www.codeplex.com/clraddins/WorkItem/View.aspx?WorkItemId=1951">reported it as such</a>). Moreover, common utility code for adapters cannot be placed into a separate assembly and placed into each adapter's folder because MAF will not load it (unless that assembly is placed into the GAC).</p> <p>For these reasons, I had to jump through some hoops to factor out this code. Basically, if you look under the "Pipeline\Shared Code" solution folder you'll see some source code files. These files are then linked to from the adapter projects. In effect, the code is stored in one place (good for maintenance) but compiled into two separate projects (to appease MAF).</p> <p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="521" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SEvIt42RJ0I/AAAAAAAAAtI/DFIydi7JdyI/image%5B19%5D.png?imgmax=800" width="600" border="0" /> </p> <p>The utility code itself isn't as clean as it would be if MAF supported adapters with generic base types. However, it's still pretty straightforward. Adapters implement one of the two interfaces (<code>IContractToViewAdapter<TContract></code> or <code>IViewToContractAdapter<TView></code>) according to what type of adapter they are. From there, the static <code>Adapt</code> class can be used to convert views to contract and vice-versa. This cuts down on a lot of code repetition, but - again - not as much as I'd like.</p> <p>The other option I had was to use a <a href="http://www.codeplex.com/Gauntlet">simple code generation tool</a>. In a "real" project I would probably do exactly that, but I didn't want to complicate these examples.</p> <p><strong>The Add-in</strong></p> <p>Included in the solution is a very simple add-in that simply outputs some information when it is initialized by the host (via the <code>Initialize()</code> method):</p> <pre class="code">[<span style="color: rgb(43,145,175)">AddIn</span>(<span style="color: rgb(163,21,21)">"Sample add-in"</span>)]
<span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">class</span> <span style="color: rgb(43,145,175)">AddIn</span> : <span style="color: rgb(43,145,175)">IApplicationAddIn
</span>{
<span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">void</span> Initialize(<span style="color: rgb(43,145,175)">IHost</span> host)
{
<span style="color: rgb(43,145,175)">Console</span>.WriteLine(<span style="color: rgb(163,21,21)">"This is the sample add-in, running inside host with name '{0}', version {1}. AppDomain name is '{2}'."</span>, host.Name, host.Version, <span style="color: rgb(43,145,175)">AppDomain</span>.CurrentDomain.FriendlyName);
}
}</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>As you can see, it outputs the details of the host (name and version) and the name of the <code>AppDomain</code> in which it is running (to prove we're being hosted in a separate <code>AppDomain</code>).</p>
<p><strong>The Host</strong></p>
<p>The <em>Host </em>project is a console application that activates and initializes any add-ins it finds that implement the <code>IApplicationAddIn</code> interface. The most pertinent code is contained in the <code>Host.Run()</code> method. This is the code that updates the add-in store, finds and activates any add-ins, and then initializes them.</p>
<p>When you execute this project, you should see something like this:</p>
<p><a href="http://lh5.ggpht.com/kent.boogaart/SEvIwfAcOkI/AAAAAAAAAtM/KPZpks4zohc/s1600-h/image%5B30%5D.png"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="100" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SEvIxZmsqOI/AAAAAAAAAtQ/kVAyR110_kE/image_thumb%5B12%5D.png?imgmax=800" width="677" border="0" /></a> </p>
<p><strong>Deployment</strong></p>
<p>MAF has strict requirements on how the pipeline and add-ins are deployed. If you've read through the MSDN documentation, you'll see recommendations on creating a solution-level <em>output </em>folder and deploying all DLLs within there. I don't like this recommendation because it's non-standard and doesn't deal with multiple configurations (although you could have <em>output\Debug</em> and <em>output\Release </em>folders).</p>
<p>In order to remain somewhat standard and not cause too much confusion, my solution builds into the output folder of the <em>Host </em>project, as you'd expect. Under <em>bin\Debug </em>(or <em>bin\Release</em>) you'll find directories called <em>Pipeline </em>and <em>AddIns</em>. Pipeline segments and add-ins are automatically copied to these directories when you build the solution.</p>
<p>If you're wondering how these files are copied, take a look in one of the project files. For example, crack open the <em>SampleAddIn.csproj </em>file and you'll see this at the end:</p>
<pre class="code"><span style="color: rgb(0,0,255)"><</span><span style="color: rgb(163,21,21)">Target</span><span style="color: rgb(0,0,255)"> </span><span style="color: rgb(255,0,0)">Name</span><span style="color: rgb(0,0,255)">=</span>"<span style="color: rgb(0,0,255)">AfterBuild</span>"<span style="color: rgb(0,0,255)">>
<</span><span style="color: rgb(163,21,21)">Copy</span><span style="color: rgb(0,0,255)"> </span><span style="color: rgb(255,0,0)">SourceFiles</span><span style="color: rgb(0,0,255)">=</span>"<span style="color: rgb(0,0,255)">$(OutputPath)\SampleAddIn.dll</span>"<span style="color: rgb(0,0,255)"> </span><span style="color: rgb(255,0,0)">DestinationFolder</span><span style="color: rgb(0,0,255)">=</span>"<span style="color: rgb(0,0,255)">..\Host\$(OutputPath)\AddIns\SampleAddIn</span>"<span style="color: rgb(0,0,255)">/>
</</span><span style="color: rgb(163,21,21)">Target</span><span style="color: rgb(0,0,255)">>
</span></pre>
<a href="http://11011.net/software/vspaste"></a>
<p><strong>Conclusion</strong></p>
<p>Well, this has turned into a long post - and we haven't even started yet! MAF is a complex beast - <a href="http://kentb.blogspot.com/2008/04/how-to-waste-3-hours-on-wednesday.html">incredibly frustrating at times</a>, and yet very rewarding at others. Hopefully I have set the stage for the subsequent posts, where I will look at how some specific application scenarios can be addressed in MAF.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-84039550481194245672008-06-05T19:59:00.001+01:002008-06-05T19:59:35.140+01:00MAF Gymnastics: Prologue<p>The Managed Add-in Framework (MAF) was introduced in .NET 3.5. In short, it is a framework that aims to ease the burden of supporting an add-in ecosystem within your application. It provides features around add-in discovery, loading, and isolation.</p> <p>The benefits of a good add-in model include:</p> <ul> <li>Security. Add-ins can be isolated and sand-boxed, each with its own set of permissions. </li> <li>Stability. Isolation also means add-ins have a limited ability to affect each other. If one add-in fails, it does not necessarily affect other add-ins. In addition, it is often possible to switch add-ins from AppDomain isolation to process isolation, which affords even more stability. </li> <li>Version tolerance. The host application can version independently of add-ins. If the host is developed with care, an add-in written against version 1 of the host can still work against version 2 of the host. </li> <li>User experience. With add-ins isolated into separate .NET AppDomains, it becomes possible to load and unload add-ins without requiring the host application be restarted. </li> </ul> <p>The key to achieving these benefits is AppDomain isolation. Without it, most of these benefits cannot be realised (version tolerance is the exception). However, supporting AppDomain isolation in your application and add-ins is no walk in the park. It introduces a slew of complexities that will have you tearing your hair out in no time.</p> <p>This series of blog posts addresses some MAF scenarios that I believe are quite common, but have not so obvious an implementation. I haven't finalized the set of scenarios I'll be covering, but I have two written, with a further two in mind.</p> <p>Note that I do assume the reader has a fundamental understanding of MAF throughout this series. If you're just getting started with MAF, you should check out <a href="http://msdn.microsoft.com/library/bb384241.aspx">this section in MSDN</a> and read this series later.</p> <p>In the next post I'll provide a skeletal MAF solution which we'll be using as a starting point for subsequent posts.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-91704804815479684062008-06-04T19:35:00.002+01:002008-06-05T14:49:24.106+01:00PS3 Power Consumption<p>From <a href="http://news.yahoo.com/s/nm/20080603/wr_nm/power_electronics_study_dc">this article</a>:</p> <blockquote> <p><em>Our tests found that leaving a Playstation 3 on while not in use would cost almost... five times more than it would take to run a refrigerator for the same yearly period</em></p> </blockquote> <p>And, in response, I have this to say:</p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SEbgVggDPwI/AAAAAAAAAss/yvsk7YgjE70/s1600-h/P6050222%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="483" alt="P6050222" src="http://lh4.ggpht.com/kent.boogaart/SEbgXQNgX8I/AAAAAAAAAsw/Eu9AictpPkU/P6050222_thumb%5B1%5D.jpg?imgmax=800" width="644" border="0" /></a> </p> <p>Yep, that's my PS3 power consumption in standby. A <em>massive</em> <strong>4 watts</strong>. And, in comparison, here is my microwave in standby (I can't reach my fridge socket):</p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SEbgZw9JJpI/AAAAAAAAAs0/ZaAgAD4FnOs/s1600-h/P6050224%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="483" alt="P6050224" src="http://lh5.ggpht.com/kent.boogaart/SEbgbHDi5nI/AAAAAAAAAs4/NL361pqNO0c/P6050224_thumb%5B1%5D.jpg?imgmax=800" width="644" border="0" /></a> </p> <p>All I can think of is they measured with wireless on, which may consume more power, but if they at least published specifics that would be helpful. I couldn't track down the supposed original article on choice.com.au.</p> <p><strong>UPDATE: </strong>Nope, just tested and even with wireless on it still uses a measly 4 watts when on standby.</p><p><strong>UPDATE 2: </strong>OK, I tracked down the original article. It's <a href="http://www.choice.com.au/viewArticle.aspx?id=106346&catId=100245&tid=100008&p=1&title=Computers%27+energy+costs">here</a>. It turns out they were misquoted in the yahoo post. The incorrect quote is "an Australian consumer agency study has found that videogame consoles ... are major electricity guzzlers, <strong>even when left on stand-by</strong>". However, the article itself points out that you should put consoles on stand by when not in use. Wow, revolutionary advice. Who the hell would leave a console on when they're not playing it? That's like leaving the TV on and muting it when you're not watching it. Newsflash: refrigerators are designed to be on 24/7; consoles are not.</p>Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-85569444247168648682008-05-18T17:53:00.001+01:002008-05-18T18:31:16.445+01:00Gauntlet V1 Released<p>Today I released V1 of Gauntlet, which is a .NET code generation tool that integrates seamlessly into your build process. I've been working away at it on and off (mainly off) for a few months now, so it felt somewhat therapeutic to finally release it.</p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SDBn4BonEOI/AAAAAAAAAsc/Ajj9tIerBGw/s1600-h/CodeGenerationProcess%5B4%5D.png"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="255" alt="CodeGenerationProcess" src="http://lh3.ggpht.com/kent.boogaart/SDBn4xonEPI/AAAAAAAAAsk/PT0jc8HgUR4/CodeGenerationProcess_thumb%5B2%5D.png?imgmax=800" width="640" border="0" /></a> </p> <p>There are a wide variety of uses for Gauntlet, but here are a couple of examples:</p> <ul> <li>Gaining control over the proxy classes generated for web / WCF services </li> <li>Generating large parts of your domain / business layer </li> <li>Generating constants or enumerations from sample data</li> </ul> <p>Head on over to the <a href="http://www.codeplex.com/Gauntlet">Gauntlet CodePlex site</a> for more information.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-39297532131795980222008-05-15T19:47:00.001+01:002008-05-15T19:47:10.208+01:00Expander and VisualBrush<p>Today I was trying to implement some drag and drop spiffiness by including a semi-transparent preview of the item being dragged. Basically when the user starts dragging something, it "tears off" and follows the mouse cursor, so they can see exactly what they're dragging. Like I said: spiffy. Don't deny it.</p> <p>Sounds like a job for <a href="http://msdn.microsoft.com/en-us/library/system.windows.media.visualbrush.aspx">VisualBrush</a>, and indeed it is. I was able to get this working pretty nicely, but noticed a weird problem with <code>Expander</code>s. Basically, if you expand and then collapse an <code>Expander</code>, and then use a <code>VisualBrush</code> to clone it, the <code>VisualBrush</code> will clone both the header of the <code>Expander</code>, and the white space the <code>Expander</code> previously occupied when expanded. </p> <p><em>Huh?</em></p> <p>Let me explain with pictures. Say you've got an <code>Expander</code>: </p> <p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="161" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SCyFJRonEKI/AAAAAAAAAr8/K_QtCGfjW0M/image%5B44%5D.png?imgmax=800" width="258" border="0" /></p> <p>And you then expand it, and subsequently collapse it: </p> <p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="161" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SCyFKBonELI/AAAAAAAAAsE/qz10kiYd1Wo/image%5B38%5D.png?imgmax=800" width="258" border="0" /> <img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="161" alt="image" src="http://lh5.ggpht.com/kent.boogaart/SCyFJRonEKI/AAAAAAAAAr8/K_QtCGfjW0M/image%5B44%5D.png?imgmax=800" width="258" border="0" /> </p> <p>Then then your code creates a <code>VisualBrush</code> around the <code>Expander</code> like this: </p> <pre class="code"><span style="color: rgb(43,145,175)">Rect</span> rect = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">Rect</span>(0, 0, _expander.RenderSize.Width, _expander.RenderSize.Height);
<span style="color: rgb(43,145,175)">VisualBrush</span> visualBrush = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">VisualBrush</span>(_expander);
_rectangle.Fill = visualBrush;</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>What you get looks like this: </p>
<p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="161" alt="image" src="http://lh6.ggpht.com/kent.boogaart/SCyFKhonEMI/AAAAAAAAAsM/cEcoxTIrjE8/image%5B50%5D.png?imgmax=800" width="258" border="0" /> </p>
<p>What the...? What's happened here is what I described above. The size of the <code>Rectangle</code> hosting the <code>VisualBrush</code> is correct. However, the snapshot the <code>VisualBrush</code> has taken of the <code>Expander</code> is incorrect because it includes white-space below the header, even though the <code>Expander</code> is collapsed. </p>
<p>As I discovered, <a href="http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1069807&SiteID=1">other people have come up against this problem too</a>, but no solution was apparent. Thus, I set about to find one. </p>
<p>And here it is: </p>
<pre class="code"><span style="color: rgb(43,145,175)">Rect</span> rect = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">Rect</span>(0, 0, _expander.RenderSize.Width, _expander.RenderSize.Height);
<span style="color: rgb(43,145,175)">VisualBrush</span> visualBrush = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">VisualBrush</span>(_expander);
<strong>visualBrush.ViewboxUnits = <span style="color: rgb(43,145,175)">BrushMappingMode</span>.Absolute;
visualBrush.Viewbox = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">Rect</span>(_expander.TranslatePoint(rect.TopLeft, <span style="color: rgb(0,0,255)">this</span>), rect.Size);
</strong>
_rectangle.Fill = visualBrush;</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>This time we get the expected result: </p>
<p><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="161" alt="image" src="http://lh4.ggpht.com/kent.boogaart/SCyFLBonENI/AAAAAAAAAsU/qy_zZzK10v8/image%5B56%5D.png?imgmax=800" width="258" border="0" /> </p>
<p>The trick is to use absolute units for the view box, and to translate the <code>Rectangle</code> relative to the host. This doesn't really explain the bug, but it does work around it. You can download the sample project (pictured in the images above) right about <a href="http://www.users.on.net/~kentcb/visualbrush/VisualBrushBug.zip">here</a>.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-56548576507643669622008-04-28T20:09:00.001+01:002008-04-28T20:11:35.802+01:00Dispatcher Frames<p>With scant exception, WPF controls must be accessed from the same thread on which they were created. Because of this, we say they have 'thread affinity'. </p> <p>In WPF, thread affinity is realised via <code>DispatcherObject</code>s, which can be accessed directly only on the thread on which they were created. Alternatively, they can be accessed via their <code>Dispatcher</code>. The <code>Dispatcher</code> contains methods for adding operations to a queue which is processed on the <code>Dispatcher</code>'s thread. Each thread has at most one <code>Dispatcher</code> instance associated with it (which is created on demand). Many WPF classes - including controls such as <code>TextBox</code> and <code>Button</code> - inherit from <code>DispatcherObject</code>. This means that accessing instances of these controls must be done on the control's thread, possibly via the control's <code>Dispatcher</code>. </p> <p>You may have noticed a type called <code>DispatcherFrame</code> whilst browsing MSDN (as you may be inclined to do on a Saturday night. Or not). That's what I want to discuss in this post. </p> <p>You can think of a <code>DispatcherFrame</code> as something that forces operations to be processed until some condition is met. Every WPF application has at least one <code>DispatcherFrame</code> that is created when you run the application. It will continue pumping operations until the application is shut down. </p> <p>At times it can be useful to pump operations until some condition of your own is met. Perhaps your code is executing on the UI thread and you want to empty the queue of operations in the <code>Dispatcher</code> to force control and screen updates. In the Winforms world, this is achieved via a call to <font face="Courier New">Application.DoEvents()</font>. In WPF, there is no equivalent method, but we can easily simulate one: </p> <pre class="code"><span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">static</span> <span style="color: rgb(0,0,255)">void</span> DoEvents(<span style="color: rgb(0,0,255)">this</span> <span style="color: rgb(43,145,175)">Application</span> application)
{
<span style="color: rgb(43,145,175)">DispatcherFrame</span> frame = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">DispatcherFrame</span>();
<span style="color: rgb(43,145,175)">Dispatcher</span>.CurrentDispatcher.BeginInvoke(<span style="color: rgb(43,145,175)">DispatcherPriority</span>.Background, <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">ExitFrameHandler</span>(frm => frm.Continue = <span style="color: rgb(0,0,255)">false</span>), frame);
<span style="color: rgb(43,145,175)">Dispatcher</span>.PushFrame(frame);
}
<span style="color: rgb(0,0,255)">private</span> <span style="color: rgb(0,0,255)">delegate</span> <span style="color: rgb(0,0,255)">void</span> <span style="color: rgb(43,145,175)">ExitFrameHandler</span>(<span style="color: rgb(43,145,175)">DispatcherFrame</span> frame);</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>The above code is part of a small demo project, which you can download <a href="http://www.users.on.net/~kentcb/doevents/DoEvents.zip">here</a>. It adds a <code>DoEvents()</code> method to WPF's <code>Application</code> class. The <code>DoEvents()</code> method adds a <code>Background</code> priority operation to the <code>Dispatcher</code>'s queue, and then pushes a new frame that pumps operations until that operation is processed. As a result, all other operations that were already queued in the <code>Dispatcher</code> will be processed. There are several important things I need to point out here: </p>
<p>1. Adding the new operation to the <code>Dispatcher</code> is done asynchronously. As such, the operation is not processed until we call <code>PushFrame()</code>.
<br />2. The choice of <code>Background</code> as the priority is important. If we were to choose a higher priority (such as <code>Render</code>), then we would potentially leave lower-priority operations in the queue after the frame had exited. That's because our operation would be executed prior to those lower-priority operations, and thus we'd end the frame too soon. Perhaps there's value in adding a <code>DoEvents()</code> overload that accepts a minimum priority of operations that should be executed, but I haven't included one in the download.
<br />3. Operations in a <code>Dispatcher</code> are not associated with a particular <code>DispatcherFrame</code>. A <code>DispatcherFrame</code> can cause any queued operation to be processed, not just those added after the frame is pushed.
<br />4. The code assumes it is running on the thread whose <code>Dispatcher</code> should be pumped. </p>
<p>A <code>DoEvents()</code> implementation is nice for illustrating the utility of <code>DispatcherFrame</code>s (indeed it is used in the MSDN documentation too), but I believe it is unnecessary. As you can see in the demo, it is possible to simulate <code>DoEvents()</code> in a much simpler fashion: by synchronously adding a low-priority no-op to the <code>Dispatcher</code>'s queue: </p>
<pre class="code"><span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">static</span> <span style="color: rgb(0,0,255)">void</span> DoEvents(<span style="color: rgb(0,0,255)">this</span> <span style="color: rgb(43,145,175)">Application</span> application)
{
<span style="color: rgb(43,145,175)">Dispatcher</span>.CurrentDispatcher.Invoke(<span style="color: rgb(43,145,175)">DispatcherPriority</span>.Background, <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">VoidHandler</span>(() => { }));
}
<span style="color: rgb(0,0,255)">private</span> <span style="color: rgb(0,0,255)">delegate</span> <span style="color: rgb(0,0,255)">void</span> <span style="color: rgb(43,145,175)">VoidHandler</span>();</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>The call to <code>Invoke()</code> will not return until our operation has been processed, which won't be until all higher priority operations are processed. Therefore, the end result is the same - all queued operations are pumped (apart from those with a priority lower than <code>Background</code>, but I'm ignoring that in this post). </p>
<p>So where would you use a <code>DispatcherFrame</code>? </p>
<p>I recently had need for one at my day job. We have a splash screen that usually just shows progress information (downloading files, searching for modules etcetera) but sometimes needs to ask the user which catalog they would like to load (a catalog is just a set of modules). </p>
<p>The splash presenter tries as hard as it can not to ask the user (if they only have one catalog, or it has been specified via a command line switch then it doesn't need to ask), but if it does need to, it delegates a call to the splash view on the UI thread. This call needs to return the catalog that the user selected. However, it has been called on the UI thread, so obtaining a catalog from the user requires pumping operations so they can select one from a list. The code looks like this: </p>
<pre class="code"><span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(43,145,175)">Catalog</span> SelectCatalog(<span style="color: rgb(43,145,175)">ICollection</span><<span style="color: rgb(43,145,175)">Catalog</span>> availableCatalogs)
{
StatusLabel = <span style="color: rgb(163,21,21)">"Please select a catalog:"</span>;
_catalogsItemsControl.ItemsSource = availableCatalogs;
_catalogSelectionFrame = <span style="color: rgb(0,0,255)">new</span> <span style="color: rgb(43,145,175)">DispatcherFrame</span>(<span style="color: rgb(0,0,255)">true</span>);
<span style="color: rgb(43,145,175)">Dispatcher</span>.PushFrame(_catalogSelectionFrame);<br /><br />
StatusLabel = <span style="color: rgb(163,21,21)">"Thank you"</span>; <br /> _catalogsItemsControl.ItemsSource = <span style="color: rgb(0,0,255)">null</span>;
<span style="color: rgb(0,0,255)">return</span> _selectedCatalog;
} </pre>
<p>This method sets up the catalogs for the user to select from by assigning them to an <code>ItemsControl</code> (which renders each catalog as a hyperlink). It then pushes a new <code>DispatcherFrame</code>, which blocks until the frame is stopped. After the <code>DispatcherFrame</code> finishes, the selected catalog is returned. The only way to end the frame is by clicking on one of the hyperlinks representing the catalogs. Each hyperlink rendered by the <code>ItemsControl</code> has a <code>Click</code> handler as follows: </p>
<pre class="code"><span style="color: rgb(0,0,255)">private</span> <span style="color: rgb(0,0,255)">void</span> _hyperlink_Click(<span style="color: rgb(0,0,255)">object</span> sender, <span style="color: rgb(43,145,175)">EventArgs</span> e)
{
_selectedCatalog = (sender <span style="color: rgb(0,0,255)">as</span> <span style="color: rgb(43,145,175)">Hyperlink</span>).DataContext <span style="color: rgb(0,0,255)">as</span> <span style="color: rgb(43,145,175)">Catalog</span>;
_catalogSelectionFrame.Continue = <span style="color: rgb(0,0,255)">false</span>;
} </pre>
<p>When a hyperlink is clicked, I store the selected catalog and then end the <code>DispatcherFrame</code>. As a result, the code that pushed the <code>DispatcherFrame</code> will continue execution. Note that I would not have to manually track the selected catalog if I was using a <code>ListBox</code>, but for now I'm using with the <code>ItemsControl</code> because it was simpler to style. </p>
<p>Above I suggested that the only way to end the <code>DispatcherFrame</code> was by setting <code>DispatcherFrame.Continue</code> to <code>false</code>. That's not the entire story. <code>DispatcherFrame</code>s may also be asked to stop during application shutdown. When that happens, <code>DispatcherFrame</code>s will, by default, comply with the request. However, there is a constructor overload that allows you to bypass this behaviour and instead continue processing operations until your condition is met. This might be useful for a short-lived frame whose completion is critical to the correctness of your application.</p>
<p><a href="http://www.users.on.net/~kentcb/doevents/DoEvents.zip">Download Demo</a></p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-42218704830493829012008-04-19T12:51:00.001+01:002008-04-19T12:51:31.967+01:00Berlin<p>Last weekend we headed to Berlin. We stayed south-east of the city, in the appropriately-named <em>Holiday Inn</em>. Friday night was basically a write-off, since we didn't arrive until around 10PM. On Saturday we had a late breakfast and then headed into the City.</p> <p>Unfortunately, the <a href="http://en.wikipedia.org/wiki/S9_(Berlin)">S9 line</a> which connected us to the centre of the city was under weekend maintenance, so it was a time-consuming process getting anywhere. This is one of the train stations we waited at (Betriebsbahnhof Schöneweide):</p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAncJe6VsWI/AAAAAAAAAm4/aGnWTvJ7inc/s1600-h/P4120023%5B4%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4120023" src="http://lh6.ggpht.com/kent.boogaart/SAncK-6VsXI/AAAAAAAAAnA/j_N79MGhMmY/P4120023_thumb%5B2%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh6.ggpht.com/kent.boogaart/SAncL-6VsYI/AAAAAAAAAnI/v_cmm7TGP3w/s1600-h/P4120025%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4120025" src="http://lh4.ggpht.com/kent.boogaart/SAncNe6VsZI/AAAAAAAAAnQ/nmQJU2Tmv54/P4120025_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p>Eventually we made it into the city and headed to the <a href="http://en.wikipedia.org/wiki/Berlin_Zoo">Berlin Zoo</a>, which was fantastic:</p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SAncPu6VsaI/AAAAAAAAAnY/8LqnzIT5EW0/s1600-h/P4120031%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4120031" src="http://lh3.ggpht.com/kent.boogaart/SAncQO6VsbI/AAAAAAAAAng/LNysHtVaAHs/P4120031_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAncRe6VscI/AAAAAAAAAno/8A7ndJ2CoJo/s1600-h/P4120036%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4120036" src="http://lh3.ggpht.com/kent.boogaart/SAncSO6VsdI/AAAAAAAAAnw/rhoitOGHNVs/P4120036_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh6.ggpht.com/kent.boogaart/SAncS-6VseI/AAAAAAAAAn4/F7Xot_vhCwI/s1600-h/P4120039%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4120039" src="http://lh5.ggpht.com/kent.boogaart/SAncTu6VsfI/AAAAAAAAAoA/MzKF2rKCW20/P4120039_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SAncVO6VsgI/AAAAAAAAAoI/pAkxM95246I/s1600-h/P4120044%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4120044" src="http://lh4.ggpht.com/kent.boogaart/SAncWe6VshI/AAAAAAAAAoQ/9gn1z5_BUMs/P4120044_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAncXe6VsiI/AAAAAAAAAoY/MIkHcK0_aBU/s1600-h/P4120060%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4120060" src="http://lh3.ggpht.com/kent.boogaart/SAncYO6VsjI/AAAAAAAAAog/D7ycWHunjDw/P4120060_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> (poor photo, but I just love black panthers. All cats want to be them)</p> <p><a href="http://lh6.ggpht.com/kent.boogaart/SAncZ-6VskI/AAAAAAAAAoo/XQHR_lAe25c/s1600-h/P4130072%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130072" src="http://lh5.ggpht.com/kent.boogaart/SAncau6VslI/AAAAAAAAAo0/U1FJOiRuXNs/P4130072_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh6.ggpht.com/kent.boogaart/SAncb-6VsmI/AAAAAAAAAo8/mOOtpKcqQ74/s1600-h/P4130076%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130076" src="http://lh5.ggpht.com/kent.boogaart/SAnccu6VsnI/AAAAAAAAApE/GBOBSMITttY/P4130076_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh6.ggpht.com/kent.boogaart/SAncd-6VsoI/AAAAAAAAApM/zh58oIjni9o/s1600-h/P4130087%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130087" src="http://lh4.ggpht.com/kent.boogaart/SAncee6VspI/AAAAAAAAApU/43xutWkJmYE/P4130087_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SAncfu6VsqI/AAAAAAAAApc/N_ZFOantmBk/s1600-h/P4130094%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130094" src="http://lh4.ggpht.com/kent.boogaart/SAncge6VsrI/AAAAAAAAApk/iuNfT5RBQRw/P4130094_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAnche6VssI/AAAAAAAAAps/8XfNsDjTvuI/s1600-h/P4130097%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4130097" src="http://lh3.ggpht.com/kent.boogaart/SAnciO6VstI/AAAAAAAAAp0/dGsb3wXouqc/P4130097_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAncje6VsuI/AAAAAAAAAp8/wPtYHV5WyYo/s1600-h/P4130104%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130104" src="http://lh3.ggpht.com/kent.boogaart/SAnckO6VsvI/AAAAAAAAAqE/d64OQetC-I0/P4130104_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SAncmu6VswI/AAAAAAAAAqM/iATKkCw3Omk/s1600-h/P4130106%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130106" src="http://lh4.ggpht.com/kent.boogaart/SAncne6VsxI/AAAAAAAAAqU/NvAbhtpTxLw/P4130106_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SAncou6VsyI/AAAAAAAAAqc/AXJXMvWpmfY/s1600-h/P4130109%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130109" src="http://lh3.ggpht.com/kent.boogaart/SAncpO6VszI/AAAAAAAAAqk/P04idSYR4KI/P4130109_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SAncqO6Vs0I/AAAAAAAAAqs/n6xAPAXMJIY/s1600-h/P4130111%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4130111" src="http://lh6.ggpht.com/kent.boogaart/SAncq-6Vs1I/AAAAAAAAAq0/Y4owM6o_CqM/P4130111_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SAncsO6Vs2I/AAAAAAAAAq8/zczXZm7EDhg/s1600-h/P4130118%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130118" src="http://lh6.ggpht.com/kent.boogaart/SAncs-6Vs3I/AAAAAAAAArE/L-BqRRYm_RA/P4130118_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a> </p> <p>On Sunday we wanted to visit <a href="http://en.wikipedia.org/wiki/Fernsehturm">Fernsehturm</a>, which is a 365 metre high tower in central Berlin. After waiting for 40 minutes or so, we gave up because the line wasn't moving. However, here are some pictures of the tower:</p> <p><a href="http://lh4.ggpht.com/kent.boogaart/SAncte6Vs4I/AAAAAAAAArM/qVDaSS0pcR4/s1600-h/P4130174%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4130174" src="http://lh6.ggpht.com/kent.boogaart/SAnct-6Vs5I/AAAAAAAAArU/IW-19PANQTM/P4130174_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh5.ggpht.com/kent.boogaart/SAncuu6Vs6I/AAAAAAAAArc/FFgDtuR8aOA/s1600-h/P4130183%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="260" alt="P4130183" src="http://lh6.ggpht.com/kent.boogaart/SAncu-6Vs7I/AAAAAAAAArk/pDAveytmClc/P4130183_thumb%5B1%5D.jpg?imgmax=800" width="200" border="0" /></a> </p> <p><a href="http://lh3.ggpht.com/kent.boogaart/SAncwO6Vs8I/AAAAAAAAArs/fcizONkqYqo/s1600-h/P4130179%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P4130179" src="http://lh5.ggpht.com/kent.boogaart/SAncwu6Vs9I/AAAAAAAAAr0/uTv5mjIH9JE/P4130179_thumb%5B1%5D.jpg?imgmax=800" width="260" border="0" /></a></p> <p>The people in Berlin were very nice, and the weather was perfect. Overall it was a great weekend, although we would have liked more time to fully appreciate it.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-23275395828764416592008-04-18T18:19:00.001+01:002008-04-18T18:19:55.138+01:00SCSFContrib 1.5<p>Admittedly, it has been far too long, but the <a href="http://www.codeplex.com/scsfcontrib">SCSFContrib</a> team have finally released version 1.5. The highlights of this release are:</p> <ul> <li><b>New Features</b> <ul> <li>All solution and projects migrated to <b>Visual Studio 2008</b> and <b>.NET Framework 3.5</b>. </li> <li><b>DockPanelWorkspace</b> and <b>FormWorkspace</b> added to <b>SCSFContrib.CompositeUI.WinForms</b> project. </li> <li><b>Action Catalog Service</b> added to <b>SCSFContrib.Services</b> project. </li> <li>Added Visual Studio templates and Installer for <b>Trusted</b> and <b>Untrusted</b> modules in the WPF/CAB Shell. </li> </ul> </li> <li><b>New Samples</b> <ul> <li><b>BankTeller</b> implementation using a pure WPF application. </li> <li><b>Demo Application</b> with its <b>Demo Script</b> that provides step-by-step instructions to create a SC-SF application. </li> <li><b>OrdersManager</b> application that demonstrates how to integrate the CAB and SC-SF with Windows Workflow Foundation. </li> <li><b>TestSuite</b> reference application showing DockPanelWorkspace usage. </li> <li><b>WPF CAB Shell</b> sample application (source code, libs and unit tests). </li> </ul> </li> </ul> <p>You can head over to the <a href="http://www.codeplex.com/scsfcontrib">SCSFContrib CodePlex site</a> to download. Enjoy!</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-61826180419586899422008-04-02T20:26:00.001+01:002008-04-02T20:26:04.205+01:00How to Waste 3 Hours on a Wednesday Afternoon<p>I'm playing around with the new to 3.5 <code>System.AddIn</code> stack at the moment. In short, it allows you to provide version tolerance between components. This is commonly used in with add-ins.</p> <p>Anyways, the thing that sits between the host and add-ins is called the pipeline. It consists of a bunch of assemblies - a host view, host adapter, contracts, add-in adapter and add-in view. If the stars are aligned and the gods bestow you with grace, you will eventually get these assemblies to work.</p> <p>Experiencing this pain, you may look to <a href="http://www.codeplex.com/clraddins/Wiki/View.aspx?title=Pipeline%20Builder&referringTitle=Home">Pipeline Builder</a>, a tool that helps to generate the views and adapters from the contract. That's what I did anyway. But I found that the lack of control over the generated code was causing me headaches (using templates would be nice), and the limitations of the tool mean that you'll end up with some generated code and some manually written. This wound up being way too painful to troubleshoot and maintain and I'd rather just have full control over the code for now.</p> <p>Sound like a plan, Stan? Since I was taking full control over the code I thought I'd go the whole hog and refactor common functionality into helper classes. Instead of an adapter that looked like this:</p> <pre class="code"><span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">class</span> <span style="color: rgb(43,145,175)">IAddInContractToViewAddInAdapter</span> : Contracts.AddInViews.IAddIn
{
<span style="color: rgb(0,0,255)">private</span> Contracts.IAddInContract _contract;
<span style="color: rgb(0,0,255)">private</span> System.AddIn.Pipeline.ContractHandle _handle;
<span style="color: rgb(0,0,255)">static</span> IAddInContractToViewAddInAdapter()
{
}
<span style="color: rgb(0,0,255)">public</span> IAddInContractToViewAddInAdapter(Contracts.IAddInContract contract)
{
_contract = contract;
_handle = <span style="color: rgb(0,0,255)">new</span> System.AddIn.Pipeline.ContractHandle(contract);
}
<span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">void</span> Initialize(Contracts.AddInViews.IHost host)
{
_contract.Initialize(Contracts.AddInSideAdapters.IHostAddInAdapter.ViewToContractAdapter(host));
}
<span style="color: rgb(0,0,255)">internal</span> Contracts.IAddInContract GetSourceContract()
{
<span style="color: rgb(0,0,255)">return</span> _contract;
}
}</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>After a bit of refactoring, I ended up with this:</p>
<pre class="code"><span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">class</span> <span style="color: rgb(43,145,175)">ContractToViewAdapter_IAddIn</span> : <span style="color: rgb(43,145,175)">ContractToViewAdapterBase</span><<span style="color: rgb(43,145,175)">IAddInContract</span>>, <span style="color: rgb(43,145,175)">IAddIn
</span>{
<span style="color: rgb(0,0,255)">public</span> ContractToViewAdapter_IAddIn(<span style="color: rgb(43,145,175)">IAddInContract</span> contract)
: <span style="color: rgb(0,0,255)">base</span>(contract)
{
}
<span style="color: rgb(0,0,255)">public</span> <span style="color: rgb(0,0,255)">void</span> Initialize(<span style="color: rgb(43,145,175)">IHost</span> host)
{
SourceContract.Initialize(<span style="color: rgb(43,145,175)">Adapt</span>.ToContract<<span style="color: rgb(43,145,175)">IHost</span>, <span style="color: rgb(43,145,175)">IHostContract</span>, <span style="color: rgb(43,145,175)">ViewToContractAdapter_IHost</span>>(host));
}
}</pre>
<a href="http://11011.net/software/vspaste"></a>
<p>Nice. The <code>ContractToViewAdapterBase<TContract></code> class provides all the functionality around exposing the source contract and maintaining the <code>ContractHandle</code>. I also have a generic <code>Adapt</code> class that takes on the task of converting views to contracts and vice-versa. The Pipeline Builder generates a separate class for every contract - yuck.</p>
<p>Once I had taken complete control of the code I managed to reduce it in size and complexity significantly. Problem is, it didn't work. All I got was this warning:</p>
<p><code>No usable HostAdapter parts could be found in assembly "...HostSideAdapters.dll".</code></p>
<p>Um, right. My host adapter was definitely in the assembly and definitely marked with the <code>HostAdapter</code> attribute. So what was going on?</p>
<p><code>System.AddIn</code> does its very best to make your life a living hell when something doesn't work. There's no trace statements, meaningful exceptions are suppressed, and if you're lucky enough to get a warning message it's usually cryptic and unhelpful. Oh, and the source and symbols for <code>System.AddIn</code> are not available yet, so you're on your own there, too.</p>
<p>After pulling some hair out I managed to figure out why my adapters weren't being found. It's because they inherit from a generic type. Yep, that's right. Even though the adapter itself is a closed type, <code>System.AddIn</code> spits it because I inherited from a generic type. Worse, it swallows a resultant <code>GenericsNotImplementedException</code> that gets thrown internally.</p>
<p>I got rid of the generic base class and instead implemented the corresponding generic interface manually. Now my adapters have more code in them and are harder to maintain. Stellar.</p>
<p>I realise this post comes across as a jab at <code>System.AddIn</code>. Actually, for the most part I really love the possibilities this stack yields. It just needs a lot of attention, especially in the area of diagnostics.</p>
<p>I've posted an issue report <a href="http://www.codeplex.com/clraddins/WorkItem/View.aspx?WorkItemId=1951">here</a>.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-15983179283855039112008-03-30T15:03:00.001+01:002008-03-30T15:03:03.131+01:00PS3<p>As a follow-up to my <a href="http://kentb.blogspot.com/2008/03/xbox-360-elite-versus-playstation-3.html">previous post</a>, I bought the PS3. There were just too many positives to ignore. I got it together with <a href="http://en.wikipedia.org/wiki/Gran_Turismo_5_Prologue">Gran Turismo 5 Prologue</a> for £300, which seemed pretty reasonable. I had to buy an HDMI cable separately, given that it doesn't come with the system. Maybe they should just ship with <em>no </em>cables, so consumers can save money buy getting only the cables they <em>need</em>.</p> <p>The setup experience was seamless. I plugged the four cables in (HDMI, Ethernet, power and USB to charge the controller) and turned it on. Once I figured out how to switch <a href="http://www.amazon.co.uk/Samsung-LE40M87BDX-XEU-Widescreen-Freeview/dp/B000O12E6S">my TV</a> to HDMI input, the PS3 setup wizard was sitting there waiting for me. And - wow - did it look beautiful. Even simple text with a background animation was looking incredibly clear in HD. The setup process was straightforward and I was good to go in a couple of minutes.</p> <p>Once the setup was done, the first thing I did was upgrade the firmware. This was a simple matter of going into the system menu and telling it to upgrade via the Internet. Then I sat back and waited while it upgraded the firmware from 2.01 to 2.2.</p> <p>Once done, I explored the menus a little and made sure everything was configured as I expected. In doing so, I was delighted to learn that there is Bluetooth support in the PS3. That will come in handy for audio during the late-night gaming sessions, since I already own some Bluetooth headphones.</p> <p>The thing that really struck me as I was looking around the menus was how fluid, intuitive and responsive they were. This is in contrast to the 360, where I often have to stare for a half second or so to comprehend which tab I'm looking at, and switching between tabs felt too slow. Someone has already recorded the PS3 menu experience <a href="http://www.youtube.com/watch?v=hCPOob3Bya4">here</a>, and it's well worth a look if you haven't used a PS3 before.</p> <p>The next thing I did was play some GT5. To be honest, I never liked the GT franchise because everything is <em>so </em>realistic . . . <em>except crashing</em>. It really kind of spoilt the games for me. You'd be caught up in the realism, only to hit a barrier at 200Kmph and bounce off it like you were playing <a href="http://www.bumperbowling.com/">bumper bowling</a> or something.</p> <p>That said, everything else about the game is outstanding. I played it a lot yesterday and plan on getting back on ASAP today (it takes a lot of willpower to write a blog post on the couch with the PS3 and TV both staring at me). I'll also be buying COD4 during the week, and jumping on GTAIV as soon as it arrives.</p> <p>At some stage yesterday, I jumped on to some multi-player GT5 too. Again, everything just worked and in no time at all I was racing against people with approximately the same driving skill as myself (ie. very little).</p> <p>Next up, I tried out the built-in web browser. I found it really quite usable. The lack of a keyboard (I haven't plugged in my USB keyboard yet) can make URI entry painful, but viewing pages and moving between links is really quite a good experience. You can zoom and pan with the controller, as well as move between links just by using the D-pad. All in all - good stuff.</p> <p>Finally, I wanted to try out the media functionality. Remember, this is the main reason I purchased this system - to access my media. At first I found the interface a little confusing. There are top-level menu items for photos, music and video. If you go to the music menu, for example, you'll still see all folder served up by the media server, which for me includes 'Videos' and 'Photos'. Navigating into these folder just gives you a message of 'There are no tracks'. Well of course there are no music tracks in my 'Videos' directory.</p> <p>I'd like to see this segregated media experience conglomerate into a single menu item: 'Media'. From there I'd like to be able to drill into specific media types or even search across my entire media library (using tags would be awesome!). For example, I could search across all media for 'London' and find all my London photos and videos (but also find <a href="http://www.youtube.com/watch?v=EGbF01lji9s&feature=related">London Bombs</a> by Eskimo Joe, unless I choose <em>not </em>to search my music media). Hopefully this experience is fixed up a bit in a future firmware release.</p> <p>I was able to view photos and listen to music straight off the bat. Awesome. However, when I tried watching video media I got a 'Media unsupported' or some such message for every video file I tried. Hmm, odd. Given that I was on firmware 2.2, all these videos should have been working. Therefore, I suspected it was an issue with my home server rather than the PS3. I tried copying those same files onto USB flash and playing directly from that stick. Doing so worked fine.</p> <p>It turns out that Windows Home Server comes with Windows Media Connect 2.0, which is outdated and doesn't support DivX or Xvid. Therefore, I'd need to install Windows Media Player 11 on my WHS box in order to stream these videos to my PS3. Fair enough, but it wasn't exactly obvious how to do this because the WMP11 installer does not allow you to install on WHS. I found <a href="http://forums.microsoft.com/windowshomeserver/showpost.aspx?postid=1642526&siteid=50&sb=0&d=1&at=7&ft=11&tf=0&pageid=2">this thread</a> (and specifically the post by <em>cigga24</em>) on the MSDN forums that lead me through the process of getting this to work. 10 minutes later I had WMP11 installed on my WHS. I tried playing the same videos on my PS3 and - lo and behold - everything worked perfectly! Moreover, the PS3 is deathly quiet, so it's a great solution for a media hub.</p> <p>I'm still not a huge fan of the PS3 controller, although I have adjusted to it a little over the past day or so. I like the analog triggers on the 360 controller, especially for car games. Ideally, I'd like a 360 controller for the PS3. I see someone has already <a href="http://news.softpedia.com/news/Download-Hack-for-Using-Xbox-360-Controller-on-PS3-43955.shtml">hacked this together</a>, but this only works inside a linux installation on the PS3 not inside games. In other words, it's pretty much useless.</p> <p>I haven't tried out the <a href="http://en.wikipedia.org/wiki/Blu_ray">Blu-ray</a> player yet. I think next weekend I'll hire <a href="http://en.wikipedia.org/wiki/Apocalypto">Apocalypto</a> and check it out. I have no doubt it's going to be awesome.</p> <p>All in all, I'm extremely pleased with the PS3 so far. There are a few interface improvements I'd like to see, but hopefully they will be fixed with a little time. I'm also going to experiment with a linux installation and geek out a bit there.</p> <p>Alright, enough typing. I'm going to play some GT5...but first, thanks to everyone for the comments in my <a href="http://kentb.blogspot.com/2008/03/xbox-360-elite-versus-playstation-3.html">previous post</a>. They were very helpful indeed.</p> <p>PS. I would post a picture but everything is a mess at the moment. I'm waiting for a proper TV stand, after which I will be able clean everything up.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-14250732037393132442008-03-30T13:14:00.001+01:002008-03-30T13:14:55.456+01:00Cardiff<p>A couple of weeks back I went to <a href="http://en.wikipedia.org/wiki/Cardiff">Cardiff</a> with some Infusion mates (or 'buddies', as most of them would say). We went to see Wales kick French butt in the <a href="http://www.planet-rugby.com/sixnations/Story/0,19022,3820_3307603,00.html">rugby final</a>. The weather was crap, but the company and atmosphere was A+. We started early and finished late. A good time was had by all (minus the French).</p> <p>I didn't post the pictures back then because - at first glance on my dinky camera screen - they looked as though they hadn't turned out. But yesterday I saw them in glorious 1080p HD via my new XBox / PS3 (I'm not telling <a href="http://kentb.blogspot.com/2008/03/xbox-360-elite-versus-playstation-3.html">which one I bought</a> yet. Not sure why - I just enjoy a bit of manufactured suspense, I guess :D ). Some of the photos <em>had </em>actually turned out quite well, so here goes:</p> <p><a href="http://lh3.google.com/kent.boogaart/R--ECSyC9OI/AAAAAAAAAk4/r6DWUdRODVg/P31502923.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3150292" src="http://lh5.google.com/kent.boogaart/R--ECyyC9PI/AAAAAAAAAlA/YTkP639tj7k/P3150292_thumb1.jpg" width="260" border="0" /></a> (Cardiff castle - I think)</p> <p><a href="http://lh4.google.com/kent.boogaart/R--EEiyC9QI/AAAAAAAAAlI/CSjIP0vOGyI/P31502933.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3150293" src="http://lh6.google.com/kent.boogaart/R--EFCyC9RI/AAAAAAAAAlQ/QvXWwGKCok8/P3150293_thumb1.jpg" width="260" border="0" /></a> </p> <p><a href="http://lh6.google.com/kent.boogaart/R--EGCyC9SI/AAAAAAAAAlY/6eAoOlhg-Ig/P31502943.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="260" alt="P3150294" src="http://lh4.google.com/kent.boogaart/R--EGiyC9TI/AAAAAAAAAlg/4aM_QVaWjdI/P3150294_thumb1.jpg" width="200" border="0" /></a> </p> <p><a href="http://lh4.google.com/kent.boogaart/R--EHiyC9UI/AAAAAAAAAlo/sxjmfZgCmA0/P31602953.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3160295" src="http://lh3.google.com/kent.boogaart/R--EISyC9VI/AAAAAAAAAlw/jvlcwi9N5bc/P3160295_thumb1.jpg" width="260" border="0" /></a> (the bar was a <em>little </em>crowded)</p> <p><a href="http://lh5.google.com/kent.boogaart/R--EJyyC9WI/AAAAAAAAAl4/4FygM4exl-U/P31602993.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3160299" src="http://lh3.google.com/kent.boogaart/R--EKSyC9XI/AAAAAAAAAmA/eKI7dHC78AY/P3160299_thumb1.jpg" width="260" border="0" /></a> </p> <p><a href="http://lh3.google.com/kent.boogaart/R--ELSyC9YI/AAAAAAAAAmI/G5QOc-Yo4F8/P31603023.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3160302" src="http://lh5.google.com/kent.boogaart/R--ELyyC9ZI/AAAAAAAAAmQ/jf-RqSZx49Q/P3160302_thumb1.jpg" width="260" border="0" /></a> </p> <p><a href="http://lh5.google.com/kent.boogaart/R--ENyyC9aI/AAAAAAAAAmY/Vy8Jpo6cfC0/P31603073.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3160307" src="http://lh6.google.com/kent.boogaart/R--EOCyC9bI/AAAAAAAAAmg/puQfpRsEW4k/P3160307_thumb1.jpg" width="260" border="0" /></a> </p> <p><a href="http://lh6.google.com/kent.boogaart/R--EPCyC9cI/AAAAAAAAAmo/Z7loENPuXQI/P31603163.jpg"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="200" alt="P3160316" src="http://lh4.google.com/kent.boogaart/R--EPiyC9dI/AAAAAAAAAmw/pp26vmeeKAE/P3160316_thumb1.jpg" width="260" border="0" /></a> (the trip back, where most everything looked like a bed)</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-55434026695626287902008-03-23T21:09:00.001Z2008-03-23T21:09:11.855ZA White Easter<p>Well, it was a <em>little </em>white, but mostly just damn cold:</p> <p><a href="http://lh4.google.com/kent.boogaart/R-bG6yyC9KI/AAAAAAAAAkY/aAWcGnBOCCc/P3230011%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P3230011" src="http://lh5.google.com/kent.boogaart/R-bG8CyC9LI/AAAAAAAAAkg/CZ35TrB8f7M/P3230011_thumb%5B1%5D.jpg" width="260" border="0" /></a> </p> <p><a href="http://lh5.google.com/kent.boogaart/R-bG9CyC9MI/AAAAAAAAAko/bOxTKnaQZGo/P3230001%5B3%5D.jpg"><img style="border-right: 0px; border-top: 0px; border-left: 0px; border-bottom: 0px" height="200" alt="P3230001" src="http://lh3.google.com/kent.boogaart/R-bG9iyC9NI/AAAAAAAAAkw/b1BNVBs-hqY/P3230001_thumb%5B1%5D.jpg" width="260" border="0" /></a> </p> <p>Happy Easter! And believe it or not, that's <a href="http://en.wikipedia.org/wiki/Vegemite">Vegemite</a> on Tempany's face - not chocolate.</p> Kent Boogaarthttp://www.blogger.com/profile/06987380257555679530noreply@blogger.comtag:blogger.com,1999:blog-7162298.post-49722352028376694992008-03-23T12:01:00.001Z2008-03-23T15:24:58.689ZXBox 360 Elite versus Playstation 3<p><a href="http://lh4.google.com/kent.boogaart/R-ZGayyC9GI/AAAAAAAAAj4/p-ilallNgM0/image%5B6%5D.png"><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="240" alt="image" src