One feature I employ often in Silverlight projects is the ability to share a model between the client and the server. When you expose a model using a WCF service, you can consume it on the Silverlight side and indicate you want to reuse types in a referenced assembly, rather than having the proxy types generated. At first glance this seems ideal because you can truly project behavior such as validation to the client without maintaining separate copies of the type in question. Upon further inspection, however, there can be some unexpected side effects.
Consider the following class that is defined in a standalone Silverlight assembly but shared on the server by adding it as a linked file:
[DataContract] public class SimpleModel { public SimpleModel() { NonMemberProperty = "This was initialized within the constructor."; } private string _field = "This is an initialized field."; public string NonMemberProperty { get; set; } [DataMember] public string MemberProperty { get; set; } public string Field { get { return _field; } } }
I’ve purposefully packed a few different examples to help illustrate what is happening. It should be very clear what will happen when you instantiate the class. The field will be initialized to expose a value, the non-member will be given a value, and the member property (the one tagged as a data member) is initialized to the default value of null.
Now you can expose the model using a web service. Just add a new “Silverlight-enabled Web Service” to your web project, and give it a very simple operation to fetch a new instance of the model:
[ServiceContract(Namespace = "http://jeremylikness.com/examples/")] [SilverlightFaultBehavior] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SimpleService { [OperationContract] public SimpleModel GetSimpleModel() { var simpleModel = new SimpleModel { MemberProperty = "This was initialized by the service." }; return simpleModel; } }
What happens now? On the server, you’ll get a new object that now has three fully populated properties – the field (which returns the value initialized on the internal field), the non-member property, and the member property. So far, so good.
Flip over to the Silverlight client side. Create a grid that has rows and columns for the labels and the values. Embed an instance of the model like this:
<Grid.DataContext> <Model:SimpleModel/> </Grid.DataContext>
And then you can easily data-bind the values like this:
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Field}" Style="{StaticResource ValueStyle}"/> <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding NonMemberProperty}" Style="{StaticResource ValueStyle}"/> <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding MemberProperty}" Style="{StaticResource ValueStyle}"/>
Add a button for the user to click. We’ll forget MVVM for now (heck, our model didn’t even implement property change notification) and just implement a code behind. Right now the model should create an instance in the designer and show you the two initialized fields — something like this:
If you do nothing in the code-behind you should still be able to run the application and see the initialized values. Now let’s get down to the wire.
Add a service reference, only make sure you specify that it re-uses types. You get to the dialog by clicking on the “advanced” button when adding a new service. It should look something like this:
This ensures that the service will return the actual type represented by the model, rather than generating a proxy type that lives in the namespace of the service itself. You get to have the full type with all of the methods and functions you’ve defined, even if they aren’t being sent over the wire. Now that’s handy, right?
In the code behind for the button click, simply fetch the instance generated on the server and update the data context:
private void Button_Click(object sender, RoutedEventArgs e) { ((Button) sender).IsEnabled = false; var client = new ServiceReference.SimpleServiceClient(); client.GetSimpleModelCompleted += _ClientGetSimpleModelCompleted; client.GetSimpleModelAsync(); } void _ClientGetSimpleModelCompleted(object sender, ServiceReference.GetSimpleModelCompletedEventArgs e) { btnLoad.IsEnabled = true; LayoutRoot.DataContext = e.Result; }
Not too complicated, right?
If you run the application and click the load button, the code-behind will call the service and fetch a new instance. When the instance is bound to the data context, however, you may notice something strange. The property that was flagged as a data member and was set on the server comes through just fine, but the other properties are empty. How can that be? Even though you didn’t tag them as data members, they should have been set when the type was constructed. The field property doesn’t even have a setter.
This is as expected, but what about the other two properties? The answer is well-documented but often overlooked. If you read the documentation for the data contract serializer, or the XML object serializer that it is based on, you’ll find a small but important sentence that explains: “When instantiating the target object during deserialization, the DataContractSerializer
does not call the constructor of the target object.” That explains the non-member property. When the serializers create the type, they by pass the normal type system. This makes sense because you are expecting to reconstruct the type based on the contract, so the properties not passed in the contract aren’t really relevant. The type is built from the ground-up by setting the properties that were serialized, but no other initialization code is called.
If you’re wondering about the field, it’s the same problem. While the C# language allows you to initialize fields as a convenience, the code is actually moved into the constructor for the type. If you decompile the DLL for the model used in this example, this is what the decompiled constructor will look like:
public SimpleModel() { this._field = "This is an initialized field."; base(); this.NonMemberProperty = "This was initialized within the constructor."; }
As you can see, the compiler conveniently moved the field initialization into the constructor. All of the initialized fields will be called from the start of the constructor before calling into the base object constructor and then handling the code you specify.
So what’s the point? It is very important that you understand the implications of sharing types between the server and the client, especially when there are behaviors you are expecting. If you have certain properties that are initialized in the constructor, you can’t expect them to be available when addressing the instances on the client. One way to deal with this is to move initialization logic into a separate method and call it from the constructor but expose it so you can call it elsewhere as well. This may work in some cases but often the reason for putting code into the constructor is to hide the implementation details of how the type is initialized from the consumer. If that’s the case, it’s probably better to make sure all of your exposed properties and methods handle initialization appropriately without a dependency on the constructor. For example, remove the initialization code for the field and replace the property code with this:
get { return _field ?? (_field = "This was initialized in the getter."); }
Now the field will be initialized on first access and never initialized again, but it will work equally well regardless of how it is instantiated. Another way to keep the initialization details private is to track the initialization state with a private field. Booleans default to false so the field will have that value whether the type was created with a call to its constructor or not. Your code can check the value of this field and call the initialization logic the first time it is needed.
Sharing types between the client and the server is a powerful feature that Silverlight provides. It is important that you understand the nuances of how it works so that you can construct your classes to behave consistently and don’t fall victim to unexpected behaviors on the client because the constructors were never called for your types.
Download the sample solution here.