November 07, 2009

Silverlight Popup positioning

I found myself frustrated recently with the positioning of Popups in Silverlight. I was surprised that web searches didn't seem to turn up accurate information, so I'll offer some things I've found about Popups hoping I'll help those that find themselves in similar circumstances (surely I'm not alone).  The position of the popup seems to be based on the following parameters:

  • The type of Parent (if parented)
  • The position within the Parent (if parented)
  • The VerticalOffset property
  • The HorizontalOffset property

The Silverlight documentation gives the impression that you must always have the Popup parented in the visual tree, which I've found not to be the case.  For instance, it seems to work to simply instantiate a Popup, set it's Child to some visual element, and set its IsOpen property to true.  I'm releived this is the case, because requiring that the parent actually be in the visual tree of essentially a different layer is a little strange at best, and at worst can lead to circumlocution of the control hierarchy to accomodate such a hack.  For instance, if a popup is to appear next to a control hosted by a content control, one would have to group the control into a panel in order to allow the popup, even though the popup doesn't have anything directly to do with the control's layout.

Anyway, if a Popup is not parented in a visual tree, the VerticalOffset and HorizontalOffset seem to work as published; namely, they represent the position of the top, left corner of the Popup relative to the global Silverlight coordinate system.  This is, IMO, the preferred method for dealing with Popup positioning if your situation allows it.  Otherwise, read on.

If the Popup is located in the "main" visual tree (ie isn't in the visual tree of another popup), the offsets are relative to that point within the visual tree.  For instance, if a Popup is located in a StackPanel underneath a Button, the Popup will appear at the location it would appear at if the Popup's child were instead at that location.  Well actually, that isn't completely precise, note that if the Popup's child were actually at the point the Popup appears, the surrounding layout might be affected by the size of the child.  To the hosting layout, the Popup appears to occupy no space.  However, the Popup's origin is not located the same as it would be if the Popup were treated as a 0 pixel object.  If the popup is given more than isn't needed space (such as in the example case of the StackPanel with a button), the Popup seems to orient itself with the top, left of its given space.  I have not experimented to see if this is only done in relation to the extent of the Popup's child size. 

The situation is more complex, however if the Popup is hosted in the visual tree of another Popup, the offset is relative to the parent popup's coordinates.  Not the parent popup's actual global coordinate, the parent popup's local coordinates.  In order to ensure that a given popup appears where it actually should be relatively to the on-screen visuals, some parent walking is required.  Here is what I've found to work:

var LPopup = FindParentPopup(this);
if (LPopup != null)
{
	var LParentPopup = FindParentPopup(LPopup);
	var LTransform = LPopup.TransformToVisual(LParentPopup == null ? null : LParentPopup);
	var LOrigin = LTransform.Transform(new Point(0d, 0d));
	Popup.VerticalOffset = LOrigin.Y;
	Popup.HorizontalOffset = LOrigin.X;
}
else
{
	Popup.VerticalOffset = 0d;
	Popup.HorizontalOffset = 0d;
}

public static Popup FindParentPopup(DependencyObject AObject)
{
var LParent = VisualTreeHelper.GetParent(AObject);
if (LParent != null)
return FindParentPopup(LParent);
else if (AObject is FrameworkElement && ((FrameworkElement)AObject).Parent is Popup)
return (Popup)((FrameworkElement)AObject).Parent;
else
return null;
}

October 09, 2009

Migrating a Remoting Service to WCF

The Need To Migrate

In extending Dataphor to include a Silverlight client, one of the biggest changes that had to be made was the communication layer. Since its initial version, Dataphor has always used .NET Remoting as the primary communication technology. However, since Silverlight does not natively support .NET Remoting, we had a choice to make. We could opt for a more primitive model and drop all the way down to sockets and take care of the messaging and marshaling ourselves, or we could migrate the existing .NET Remoting infrastructure to WCF and take advantage of Silverlight's built-in WCF support. In the end, we chose to migrate to WCF, mostly because it allowed us to increase the potential number of technologies in which a Dataphor client could be built. This post details some of the roadblocks we encountered along the way, and the solutions we came up with. Hopefully, it will shorten someone else's journey.

Asynchronicity I

The first issue to be tackled was the lack of a synchronous model for invoking a WCF service from Silverlight. The Dataphor CLI was designed as a set of interfaces, somewhat resembling a traditional DBMS CLI, with layers corresponding to the different layers of calls that can be made (such as Server, Session, Process, Cursor, etc.). Each of these layers exposes calls for performing operations against the server, and each call is by design a blocking call. Because the server supports multiple connections, asynchronous operations can be built as a layer above the CLI if necessary. However, because Silverlight does not support synchronous service invocation, we needed a way to wait for the results of every call.
 
Now, there is no shortage of material on the relative merits of synchronous versus asynchronous calling, and this post is not going to add anything to that debate. Suffice it to say that without completely re-engineering the client side, we need to be able to invoke our CLI calls synchronously. So the first step towards a solution was to verify that a simple service could be synchronously invoked from a Silverlight application. The idea was to use an invocation thread that would perform all the network communication, waiting on the AsyncResult.AsyncWaitHandle returned by the Begin call of the service operation. Once that returns, we invoke the End to get the result and voila, we have a synchronous call. So long as we keep that call off the main thread everything works fine, problem solved:
 
      IAsyncResult LResult = LService1.BeginGetData(4, null, null);
      LResult.AsyncWaitHandle.WaitOne();
      return LService1.EndGetData(LResult);
 
The reason we have to keep the call off the main thread is that all network traffic in Silverlight appears to be threaded through the main UI thread, so if you block that thread waiting for the result, you'll never get the callback. That's probably an overly simplified description of what's happening, but the solution we've come up with works fine.
 

Asynchronicity II

One of the things that became clear as we were building this proof-of-concept is that when you define an operation (at least a two-way one) in a service contract, you are really defining both a message and its associated response. As a result, a service can be invoked asynchronously on the client, even if the service contract is defined synchronously on the server. (This is probably obvious to everyone but me, so bear with me). For example, if I define the following service contract:
 
      /// <summary>
      /// Describes the interface for the Dataphor listener.
      /// </summary>
      [ServiceContract(Name = "IListenerService",
Namespace = "http://dataphor.org/dataphor/3.0/")]
      public interface IListenerService
      {
            /// <summary>
            /// Enumerates the available Dataphor instances.
            /// </summary>
            [OperationContract]
            [FaultContract(typeof(ListenerFault))]
            string[] EnumerateInstances();
      }

I can consume this service synchronously using the IListenerService interface directly, or I can define an asynchronous version:

      [ServiceContract(Name = "IListenerService",
Namespace = "http://dataphor.org/dataphor/3.0/")]
      public interface IClientListenerService
      {
            /// <summary>
            /// Enumerates the available Dataphor instances.
            /// </summary>
            [OperationContract(AsyncPattern = true)]
            [FaultContract(typeof(ListenerFault))]
            IAsyncResult BeginEnumerateInstances(
AsyncCallback ACallback, object AState);
            string[] EndEnumerateInstances(IAsyncResult AResult);
      }

Of course, this is exactly what the Add Service Reference feature of a Silverlight project in Visual Studio is doing, which leads to the conclusion that (unless the Silverlight version of the WCF communication code is substantially different than the standard .NET one) there is no technical reason that a Silverlight client couldn't invoke synchronously. Which leads to the conclusion that so long as the actual service invocation is kept off the main thread, a synchronous version of the service should work. Unfortunately, attempting to feed the synchronous version of the interface to the ChannelFactory in Silverlight gives the error "The contract 'IListenerService' contains synchronous operations, which are not supported in Silverlight…" I for one am convinced that this is not a technological limitation, just an error thrown in to try to force developers to adopt the asynchronous programming model in Silverlight.

Migrating MarshalByRefObject

The second major issue to be tackled was the fact that the Dataphor CLI uses the instancing and lifetime management services provided by .NET Remoting. Each layer of the Dataphor CLI is modeled by a MarshalByRefObject descendent that implements the interface containing the calls appropriate to that layer. WCF, on the other hand, is essentially solving a different problem, and does not have any facilities for cross-process instancing. As a result, we were faced with another decision. Either we re-engineer the entire CLI to work without instancing, or we recreate the lifetime and instance management facilities provided by .NET Remoting and expose them via a WCF service.

Because the Dataphor CLI was already layered into a 'developer-friendly' version meant to be used directly from code, and a 'network-friendly' version optimized to reduce network traffic, building the instancing and lifetime management facilities could be done relatively easily and would enable all the existing client and server side infrastructure to be used as is.

The Way It Was

First, a little background; the core CLI is defined by the IServerXXX interfaces. This is the development-level interface actually exposed to the code, and is designed to be as easy as possible to use from a development perspective. On the server-side, these interfaces are implemented directly by ServerXXX classes that make up the actual running server.

The network-level CLI is defined by the IRemoteServerXXX interfaces, and is designed to minimize network round-trips and message sizes. On the server-side, these interfaces are implemented by a set of RemoteServerXXX classes that sit on top of the ServerXXX classes and route the calls to and from the network layer.

On the client-side, the IServerXXX interfaces are implemented by the LocalXXX classes, which are responsible for consuming the IRemoteServerXXX proxies returned by the remoting layer and converting the network-level CLI back into the development-level CLI. The result is that whether a client is accessing the Dataphor Server in- or out-of-process, the programming model is identical.

The Way It Is

In order to preserve this programming model (and the mountains of code written on top of it), the WCF-enabled architecture effectively acts as a shim between the server- and client-side implementations of the IRemoteXXX interfaces.

To avoid multiple channels on the client, instead of a group of interfaces, the entire CLI is exposed via the IDataphorService interface, and each level of the CLI is modeled with handles. On the server-side, a DataphorService implements the actual service and simply wraps up the existing RemoteServerXXX classes. Each object that would have been marshaled in .NET Remoting is assigned a Handle and tracked by the DataphorService. Information that would have been marshaled via properties of those objects is now packaged in Descriptor structures.

On the client-side, the IRemoteServerXXX interfaces are implemented by ClientXXX classes that mirror the RemoteServerXXX objects on the server side. All communication is channeled through the DataphorService, and the object state is unpackaged by the ClientXXX and exposed through the IRemoteServerXXX interfaces back to the existing LocalXXX implementations. As a result, all the existing client-side code still works, it just uses WCF now instead of remoting.

Watch Out For Out

Another aspect of the .NET version of the CLI that had to be changed was the use of ref and out parameters. Of course, these work fine for the synchronous version of the service, but in the asynchronous version, the ref and out parameters were never being set. Of course, this makes sense if you think about it, but if there was ever a good place for an exception, this would be it. How about: "Ref and out parameters cannot be used with asynchronous invocation."

Lifetime Management

When the CLI was exposed via .NET Remoting, we were able to take advantage of the fact that .NET tied the lifetime of the proxies to the lifetime of the connection. Using lifetime services, if a remote object failed to renew its lease, the remoting infrastructure would disconnect the object and notify the RemoteServerXXX layer that disconnection had occurred. In the new WCF architecture, no such services exist.

It should be noted that we looked at using WCF sessions to enable this functionality and decided against it for several reasons. First, the session management built in to WCF isn't an exact fit to the way sessions are managed in the Dataphor CLI, so we would have ended up having to build a shim architecture on top of that anyway. Second, the session management required the use of the WsHttpBinding, which at least at the time of the migration, was not supported in Silverlight, our primary target for the migration in the first place.

In the .NET version of the service, we used a client-side thread that simply posted a do-nothing message (a ping, if you will) to the server on a timer. The lifetime lease for each object was set to renew for a little over twice the time of the client timer, and so long as the client could reach the server, the remote object would stay live.

In the WCF version, we left the client-side mechanism alone, and simply added a daemon to the Dataphor Service to check the last 'ping' time for each connection. If the last ping time occurred before the idle timeout, the connection is assumed to be lost and all the sessions it supported are closed.

Because the ping is running on a separate thread in the client, it will occur even when the client is busy, so the service does not need to do anything to track activity occurring below the session.

Exception Management

When the CLI was exposed via .NET Remoting, exception management was fairly simple. All the RemoteServerXXX layers had to do was make sure that any exception that hit a remoting boundary was serializable, and deserializable by the client (i.e. the exception class was available to the client app domain). We did this by making sure that all exceptions thrown across remoting were descended from our own DataphorException class, and that all relevant exception classes were available on the client (that is another story).

In the WCF implementation, however, exceptions always cross the service boundary as a fault. The simplest solution was just to turn on IncludeExceptionDetailInFaults. This was safe from the service perspective because we already knew that every exception coming out of the service was a known-good remotable exception. However, the problem was that when the exception was surfaced on the client-side, it became a FaultException<T>, with T being a basic ExceptionDetail class. There were several problems with this. First, the ExceptionDetail class only has the information carried by the base Exception. Our exception classes carry other information (such as syntax and compiler error line information, system-level error codes, etc.) and this information was being lost. And second, the client-side code downstream from the service was written to expect exceptions to be of the appropriate type.

To solve these problems, we introduced a DataphorFault. This fault class was simply a combination of all the information that could be carried by a DataphorException or any of its descendents. Then each operation contract was marked with a fault contract specifying this fault type. In the implementation of the DataphorService, each call is wrapped with a catch that converts any exception into a FaultException<DataphorFault>. With that in place on the server-side, we no longer need the IncludeExceptionDetailInFaults on the service behavior.

On the client side, each call is also wrapped with a catch block that converts any FaultException<DataphorFault> back in to the appropriate DataphorException descendent with all the relevant information from the fault. In this way, exceptions are transported across the WCF boundary without the server or client ever being the wiser. All the existing exception management code on both sides remains the same.

No Configuration Files Required

An aspect of WCF that we wanted to avoid was the astonishing proliferation of .config files that are required to enable even the simplest WCF scenarios. Of course, configurability is a good thing, but in this case, we already had configuration for the important aspects of the server (host name, server instance, port number, etc.), and we did not want the migration to WCF to add any administrative overhead if we could avoid it.

So rather than specify service behavior and endpoint configurations in config files that would become part of the deployment, we built that in programmatically. We were able to control every aspect of WCF service and hosting behavior programmatically, and added zero configuration to the deployment of a standard, network accessed Dataphor Server.

For Silverlight, we had to tackle the problem of 'cross-domain access'. For this, we simply built a Cross Domain Service to serve up a clientaccesspolicy.xml file. The only tricky part here was figuring out how to get the 'Web' behavior specified so that a URI request coming in would be treated as a web request, rather than a SOAP action. This can be done programmatically by adding a WebHttpBehavior to the behaviors of the newly created Endpoint. However, because we implemented a separate service, it was easier just to use a WebServiceHost rather than a ServiceHost.

Conclusions

So at the end of the day, what did we get out of the migration? Well, besides a deeper understanding of Yet Another Remote Procedure Call Technology From Microsoft (YARPCTFM), we did get some pretty substantial benefits:

·         Increased Exposure – A Dataphor Server can now be exposed via http/s as an industry standard Web Service. Something we never had before. And with both the standard CLI and the new Native CLI exposed, accessing a Dataphor Server is possible from pretty much any technology now known.

·         Network Resilience – Communications with a Dataphor Server are now stateless from the networking perspective. A dedicated connection is no longer required, with session management being built in the CLI and calling protocol rather than baked into the network layer. This will give Dataphor clients much greater resilience to intermittent network connections.

·         Silverlight Capability – Following from the increased exposure bullet above, it is now possible to build a Silverlight Dataphor client, a project that is nearing completion.

·         Leverage On Existing Code – By building the WCF replacement the way we did, we were able to preserve the existing Dataphor code base on both sides of the network boundary. We dropped in an entirely new communication layer and neither side knows the difference. Fantastic.

October 07, 2009

Porting a large .NET application to Silverlight

If you've followed the latest in the Dataphor wiki you'll know we're building a Silverlight client for Dataphor.  From the sound of "new client", one would think this effort would involve merely adding a new component to Dataphor, but in truth the effort is basically a port.  This is due to the fact that a Dataphor client runs a Dataphor engine, and because Silverlight doesn't support "older" .NET technologies such as remoting.  All in all, this "new client" actually entails:

  • Modernizing the Dataphor code base.  The code now makes use of generics, compiler inferred type declarations, lambda functions, etc.
  • Removal of legacy portions.
  • Splitting of the Dataphor server into Server and Engine.
  • Replacement of Remoting with WCF.
  • Adding platform support to libraries.
  • Delayed class loading.
  • Porting of the engine to Silverlight.
  • Oh, and a new Silverlight client.

This effort represented nothing short of a massive upheaval of the source base, mostly for the better.  I've not seen a detailed account of the porting of something as large as a relational database engine to Silverlight, so we kept notes along the way in hopes that this might help others.  There are of course many levels at which this port could be discussed, from architecture to syntax.  In this post, I'll just give a rough list of the runtime and BCL issues that were encountered in the port.  Perhaps we'll expand on some of these in a future post.  We may also post more about the architectural issues, such as thread implcations, in a future post.

Here's the overview: 

Issue

Resolution

Unsafe code for buffer splicing

Replaced with safe alternatives.  Added ByteArrayUtility to provide services similar to BitConverter and BinaryReader/BinaryWriter

Can't reference non-Silverlight projects from Silverlight projects

New Silverlight projects with links for each file

No HashTable and ArrayList

Replaced with Dictionary and List generics

Dictionary and List semantic differences

  1. TryGetValue versus this[] == null
  2. No virtuals for added/removed
  3. StringComparer won't work on <object, object>
  4. DictionaryEntry -> KeyValuePair
  5. No ToArray() methods on Keys and Values

No SerializationAttribute

Removed attributes, replaced with DataContract (WCF)

No SerializationInfo

Removed, exception overloads replaced with FaultContract

No StringCollection

Replaced with List<string>

P/Invoke calls to QueryPerformanceCounter APIs

Replaced with calls to Environment.TickCount

No Stopwatch class

Implemented alternative based on Environment.TickCount

No File.GetAttributes()

Moved dependency into platform specific assembly

No AppDomain.GetAssemblies()

Moved dependency into platform specific assembly

No Assembly.Load (remaining overloads are security restricted

Refactored to use AssemblyPart.Load from a byte[]

No WebRequest.GetResponse

Removed dependency

No WebClient.OpenRead, only async supported

No longer dependency

No MD5CryptoServiceProvider

Used this to replace framework implementation: http://www.markharris.net.au/blog/2008/10/23/md5cryptoserviceprovider-for-silverlight/

...Then removed because a comparison was better than a hash in usage case.

No XmlTextWriter

Replaced with XmlWriter

No XmlTextReader

Replaced with XmlReader

No XmlDocument and related

Replaced with XDocument (had to go to 3.5 framework)

No TypeDescriptor (thus no GetTypeConverter)

Replaced with StringToValue and ValueToString implementations that switch on the type and convert the native and native like types to and from strings

No Type.GetInterface overload with one argument

Used the two argument version.

No Diagnostics.Trace class

Used the Debug class instead

No String.Compare with case insenstivite

Replaced with String.Equals(x, y, StringComparison.OrdinalIgnoreCase) or similar Compare overload

No Enum.Parse w/o case specifier

Added explicit false third argument.

No remoting

Replaced with WCF

No Diagnostics.TraceLevel

Removed usage

No UnicodeEncoding.ASCII encoding

Replaced with UTF8 encoding

No 2 argument Convert.ChangeType

Passed Thread.CurrentThread.CurrentCulture as 3rd argument

No Assembly.GetType passing ignoreCase

Called another overload

No Rijndael encryption

Replaced with Aes

No Thread.Interrupt()

Removed usage

ThreadInterruptedException not public

Rewrote Interrupt pattern with AutoResetEvent()

No Environment.MachineName

Compiler conditional

No BitVector32

Removed reference (could have used BitVector)

No System.Drawing (Image etc.)

Image replaced with Media.Imaging.BitmapImage.  Image.Load() becomes BitmapImage.SetSource()

No System.Drawing attributes

Attributes redefined as dummies;

No ComponentModel attributes

Attributes redefined as dummies;

No ReaderWriterLock

Found custom implementation, modified.  Then found usage was minor and removed.

P/Invoke to Terminal Services API

Compiler defined wrapper class

ChannelFactory doesn't support overload that takes URI string

Constructed EndpointAddress as argument

ComponentCollection of IContainer doesn't support enumeration (stub)

Compiler defined usage

No ManualResetEvent.WaitOne(int, false) overload.

Replaced with invocation with just timeout.

No Assembly.CreateQualifiedName

Replaced with string concatenation of <name>,<assembly name>

No Thread.ResetAbort()

Removed reference, we no longer use abort (ever) anyway.

Assembly.GetName() is marked security critical (can't access)

Wrote parser to get Name and Version from the FullName

Can't DataMember serialize a private member

Changed to internal and used the "friend class" mechanism to allow the System.Runtime.Serialization assembly to access

Designer related attributes

Created dummy attributes; used the class name (string) overload rather than typeof() overloads to avoid dependency on designer assemblies.

No C# compiler

Ported Mono*

*This is still in-flux.  We've ported the mono compiler to Silverlight, but haven't tested it yet.  If we run into too much trouble with that, we'll invoke the compiler on the server and load the resulting binary on the client.

Here are some notes regarding the XmlDocument to XDocument porting:

Issue

Resolution

CreateElement

new XElement(…)

CreateAttribute

Element.SetAttributeValue

foreach (XmlAttribute in Element.Attributes)

foreach (XAttribute in Element.Attributes())

Node.NamespaceUri

Node.Name.NamespaceName

Node.Name

Node.Name.LocalName or Node.Name.NamespaceName

Attributes != null

HasAttributes

FirstChild, Children

Elements() enumerator

Document.DocumentElement

Document.Root

XmlDocument.Normalize()

XDocument.Normalize (not present in Silverlight) 

? may not be necessary any longer

Attributes.Remove

SetAttributeValue(x, null)

Attributes[x]

Attribute(x)

XDocument.Load(Stream)

XDocument.Load(XmlReader.Create(Stream))

(BCL doesn't support the overload SL does)

Node.Parent.RemoveChild(Node)

Node.Remove()

ChildNodes

Elements() (in most cases)

 

July 02, 2009

Progress On Dataphor

Progress on the Dataphor front has been slow but steady for a long time now. Recently, however, thanks to support from the community as well as targeted investment from Dataphor users, we have been able to make some significant progress. In addition to several defect repairs, many new features have been added.

Instancing

A running Dataphor server has always been called an instance, but recent changes have made configuring and administering multiple instances dramatically easier. Multiple instances can be installed and managed on the same machine without the need to run command-line installations, or manage configurations using config files. A new listener service has been added to make instance discovery possible from the client, and port configuration can now be managed completely server-side. In addition, several changes have been made to consolidate the data used by an instance and enable multiple instances to use the same executable and library directories.

Native CLI

A new, lightweight, potentially stateless CLI has been built to enable a Dataphor server to be accessed from applications without having to incur the overhead required by the standard CLI. Because it is intended as an alternative, not a replacement, the new CLI is not as feature-rich, but it eliminates the dependencies on the Dataphor code base and can be used virtually stand-alone. This will enable the Dataphor server to be reached from a much broader range of clients, such as mobile applications, Silverlight clients, and other environments where resources are at a premium. In addition, the stateless potential of the new Native CLI enables applications to make scaling decisions that were previously infeasible with the standard CLI.

Cross-Instance Query

The Dataphor server now supports the configuration of Server Links that can be used to query other running instances of Dataphor servers. The Dataphor Server coordinates transactions between the current instance and any number of connected instances to provide seamless nested distributed transaction support.

SQLite Catalog Support

The catalog persistence layer was abstracted to provide the ability to plug-in different catalog store implementations, and a SQLite catalog store implementation was built. The Dataphor Server can now be configured to use SQLite, rather than SQL Server Compact Edition to provide catalog persistence.

Mono Support

In order to enable the Dataphor server to be used on a Mono platform, we have taken steps to remove any platform-specific dependencies in the code base, as well as the client tools.

All these features are currently implemented in the main development branch of the Dataphor project. To try them out, simply get latest from the open source svn repository and build the Dataphor solution. We have consolidated the projects so that it's easier than ever to build and get up and running quickly.

For more complete discussion and up-to-date documentation about the latest features available in Dataphor, see the What's New in 2.2 page of the dataphor wiki.

 

May 22, 2009

Simulating SharedSizeGroup in Silverlight

There are several features of WPF that are not present in Silverlight, some of which are pretty difficult to work-around or live without.  One such feature is the SharedSizeGroup property that WPF has for column and row definitions of a layout grid.  This feature made many advanced layout scenarios entirely declarative.  Consider, for instance, the situation where an outer grid defines a set of control groupings for a form.  Within each of the out grid's cells are text boxes with left aligned labels.

SharedSizeGroup in WPF

In this case, the inner grids can use a SharedSizeGroup property on the column containing the labels and the text will be aligned automagically.

We wanted to be able to accomplish basically the same thing in Silverlight, and maybe even see if we could go about making it a little more general.  In particular we wanted to divorce the functionality from the Grid so that it could be used in non-grid situations as well.

What we came up with was a combination of two classes: SharedSizePanel and SharedSizeGroup.  SharedSizePanel is a simple, single element container which has the optional properties WidthGroup and HeightGroup.  All panels associated with a particular group will be given the size of the panel with the greatest content size in that dimension.

<local:SharedSizePanel WidthGroup="{StaticResource Horizontal}" >
  <Border Background="Coral">
    <local:ResizeBox Width="30" Height="30" />
  </Border>
</local:SharedSizePanel>

A SharedSizeGroup is a simple class that can easily be instantiated in the resources section:

<UserControl.Resources>
  <local:SharedSizeGroup x:Key="Horizontal" />
</UserControl.Resources>

We wanted to allow widths and heights to be synchronized to the same value, which would allow such fanciful things as the width and height of a particular control to be synced.  That capability would add quite a bit of complexity to what is presently a very simple implementation so we held off.  

There were a few challenges that were encountered in building this control, including:

  • Custom Measure and Arrange logic doesn't seem to work as expected as a descendant of ContentControl.  It worked as Panel descendant, but there doesn't seem to be a way to limit the panel to a single child, so fortunately it was found to work as a ContentPresenter.
  • The group cannot and must not force measurement of the panels, so it must store what the measurement was when it happened.  This is true for several reasons including the fact that the "available size" is only known when passed as an argument to the measure override.
  • As a content control, the VisualTreeHelper class must be used to get the actual element that the Content becomes.  Content of type object and you can't Measure an object.

In the end, the solution seems relatively simple and elegant, so fortunately I consider that all of the above worked out favorably. 

I haven't seen any indications that SharedSizeGroup will be introduced in Silverlight 3, and I'm not ready yet to dispose of my Silverlight 2 development environment to find out.  Anyone have version 3 installed that wouldn't mind verifying?

Download the code for these classesDownload the WPF SharedSizeGroup sample.

April 02, 2009

Silverlight 2 missing features

I've had this rant rotting in my notes for too long, time to post.  The topic is features that were in WPF but are missing from Silverlight 2.  Now that Silverlight 3 is in beta, this information may not be as timely or useful, but perhaps still useful to someone.  The below excludes items spelled out in the official documentation on the topic.  Note that although the final feature set of Silverlight 3 hasn't been set, many of these items appear missing there too.
  • DrawingVisual Brushes – this doesn’t seem like a big deal, but without this, doing something as simple as say, tiling an image, becomes very difficult.
  • Relative source binding – binding from one property of one entity to a property of another has to be constructed in code.  Hard to believe that this didn’t make the list, but fortunately is in Silverlight 3.
  • TemplateBinding to anything other than a root dependency property.
  • The DataContext is not inferred from parent to child.
  • SharedSizeGroup – this subtle, but oh so useful property of the GridColumn and GridRow Grid classes.
  • Popup.PlacementTarget – Popups have to be absolutely positioned, which is unfortunate given that there are no dialogs or forms, so Popups can be a pretty important element.
  • LayoutTransform – RenderTransforms are useful, but certainly don’t provide for what LayoutTransforms did.
  • Left mouse click – What were they thinking?!  They could have had an instant advantage over flash.
  • OverrideMetadata – This important aspect of dependency properties can be a pain to live without.
  • AddOwner – Another pretty useful dependency property trick.
  • Read-only dependency properties – Another property thing that’s been a pain to work-around.
  • Property change notification – This one is a real bear.  There seems to be no programmatic way to know that a DependencyObject’s property has changed (ouch).
  • No custom routed events – This one has been a real pain for me, but might not come up for most apps.
  • GetFlattenedPathGeometry and GetPointAtFractionLength – path functions that are possible to work around, but a pain.
  • Baseline in FontFamily
  • OnRender – They seem to have some good arguments for this one, however.
  • Keyboard Key to character code translation - that is, knowing that say Key.D1 translates to character '1'.  Once has to also check the Shift state of the keyboard, and basic keys like + are only in the PlatrofmKeyCode, which requires a series of hard-coded assumptions to deal with.

I also had a couple Silverlight 2 gotchas noted:

  • If you rename a Silverlight project, watch out that the "Startup Object" property of the project is corrected too, or the app will fail to start with no explanation.
  • If you have a web application to go with your Silverlight project, don’t add the Silverlight application as a reference, associated it under the Silverlight tab in the web apps configuration dialog.
  • If you will be creating more than one instance of a UserControl, don’t name it or you will receive and error when instancing the 2nd.  This is a royal pain because if it is unnamed it cannot be referenced by the animation objects (or by-name binding in Silverlight 3).  Basically you have to programmatically create the binding for anything that binds to the root UserControl.

Links to other similar posts: