.NET - Source code delen tussen .NET en Silverlight
- Datum:
- Auteur: Stefan Cruysberghs
- Deze pagina is enkel in het Engels beschikbaar
A common problem when developing Silverlight applications is how to share classes and in particular (Entity Framework) entities which are compiled for the full .NET framework in a Silverlight application. Silverlight is a browser plugin and a platform independent subset of the full .NET framework. Therefore Visual Studio does not allow referencing .NET assemblies from your Silverlight application.
Fortunately there is a very simple technique to share and reuse source code. In this article I will try to demonstrate this technique with several real world examples and I will give you some handy tips.
- Referencing assemblies
- Linking files in Silverlight assemblies
- Using the SILVERLIGHT compiler directive
- Sharing code between WPF and Silverlight
- Sharing (POCO) entities between your .NET domain layer and Silverlight
- Sharing extension methods between .NET and Silverlight
- Sharing logic in your Entity Framework entities between your .NET domain layer and Silverlight
- Sharing common logic and separating framework dependent implementations
Referencing assemblies
Like I stated before Visual Studio does not allow referencing full .NET assemblies (DLL) from Silverlight applications (XAP) nor Silverlight libraries. All projects will be visible in the Add Reference dialog but when you select a .NET assembly you will get an error.
The other way around works but it also has a lot of restrictions. It is possible to reference a Silverlight application/library in a full .NET assembly but the Silverlight assembly can't have any references to Silverlight framework assemblies like System.Windows. So in most cases linking files is the only good solution.
Linking files in Silverlight assemblies
The most interesting technique to share code is based on linking items.
- Just implement your classes in a full .NET project.
- Create a second project which is a Silverlight Application or a Silverlight Library.
- Open the Add Existing Item dialog from your Silverlight application or library and make sure that you select the Add As Link option.
Linked items are a little-known feature in Visual Studio but it is very handy to share files between multiple projects. The icon of a linked item will be displayed as a shortcut. Any modifications you make to a class in one project will be reflected in the other. Code insight will use the correct framework depending if you opened the original file or the linked file. Briefly, linked items are a very good approach to avoid duplicate code bases for Silverlight and .NET 3.5.
Using the SILVERLIGHT compiler directive
A second technique which is sometimes needed, is using compiler directives. The Silverlight framework is a subset of the full .NET framework so in some cases some functionalities do not exist or classes are included in different namespaces. This can be solved by surrounding this code with compiler directives to denote the code differences. There is already a special compiler directive called SILVERLIGHT.
#if SILVERLIGHT
...
#else
...
#endif
#if !SILVERLIGHT
...
#endif
Sharing code between WPF and Silverlight
At this moment there are a lot of differences between WFP XAML and Silverlight XAML. Therefore it is a hard job to reuse XAML between these two technologies. Fortunately sharing C# code is a lot easier. This first example shows a ValueConverter class which is shared between WPF and Silverlight. Silverlight does not support the ValueConversionAttribute. So you have to use the SILVERLIGHT compiler directive to skip this line when compiling the file in a Silverlight assembly.
This ValueConverter also uses the HttpUtility class. This class is supported by Silverlight but it is included in another namespace. Therefore you have to add a compiler directive to refer to System.Windows.Browser in Silverlight and to System.Web in the full .NET framework.
using System;
#if SILVERLIGHT
using System.Windows.Browser;
#else
using System.Web;
#endif
using System.Windows.Data;
namespace ScipBe.Common.Wpf
{
#if !SILVERLIGHT
[ValueConversion(typeof(string), typeof(string))]
#endif
public class HtmlDecodeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return value == null ? "" : HttpUtility.HtmlDecode((string)value);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
Sharing (POCO) entities between your .NET domain layer and Silverlight
When building a layered Silverlight application all your domain objects or entities will be assembled in a kind of domain layer. You will need to implement WCF services to send these entities or DTO’s (data transfer objects) to the Silverlight client.
Consuming a web service in a Silverlight application can be done in much the same way as in a full-fledged .NET application. By default the Add Service Reference wizard will generate code for a proxy class and for your data contract types. Thereby all your types will be duplicated. If you would like to preserve some logic in methods and properties then you have to make sure that the wizard reuses your types.
My EmployeePoco class implements an extra computed property FullName and some methods like Validate(), ToString() and a static AutoCompleteSuggestion() method which can used in the AutoCompleteBox of the Silverlight Toolkit.
Decorate the POCO class with DataContract and DataMember attributes.
using System.Runtime.Serialization;
namespace ScipBe.Demo.Domain
{
[DataContract]
public class EmployeePoco
{
[DataMember]
public int EmployeeId { get; set; }
[DataMember]
public string FirstName { get; set; }
[DataMember]
public string LastName { get; set; }
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
public override string ToString()
{
return FullName;
}
public bool Validate()
{
return ((EmployeeId > 0)
&& (!string.IsNullOrEmpty(FirstName))
&& (!string.IsNullOrEmpty(LastName)));
}
public static bool IsAutoCompleteSuggestion(string search, object item)
{
EmployeePoco emp = item as EmployeePoco;
if (emp == null)
return false;
search = search.ToUpper();
return (emp.EmployeeId.ToString().Contains(search) ||
emp.FirstName.ToUpper().Contains(search) ||
emp.LastName.ToUpper().Contains(search));
}
}
}
Once you finished the class declaration you have to link this file in a Silverlight library. Make sure that this is a different assembly then the one which will contain the service reference! Then add a web reference by starting the Add Service Reference wizard. Make sure that you check the Reuse types in referenced assemblies option and that you select your Silverlight library. This way the Add Service Reference wizard will find the types already referenced in the shared project and not generate duplicates. Now you will be able to use all methods of the EmployeePoco class in your Silverlight client.
If you would like to take a closer look at the generated code, then press the Show all files button at the top of the Solution Explorer. Now you can expand the service reference and open the Reference.cs file.
Unfortunately there is a problem with this first approach. If you would like to use two-way binding in XAML, then you have to implement the INotifyPropertyChanged interface. The INotifyPropertyChanged interface defines a single event which is called PropertyChanged. This event should be raised to notify the bound UI controls that a property has been changed. Because of this you cannot use automatic properties and you have to implement getters and setters for each property. Of course you can use code snippets but still it implies a lot of coding.
[DataContract]
public class EmployeeBinding : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private int employeeId;
[DataMember]
public int EmployeeId
{
get
{
return employeeId;
}
set
{
if (value != employeeId)
{
employeeId = value;
NotifyPropertyChanged("EmployeeId");
}
}
}
Sharing extension methods between .NET and Silverlight
The Add Service Reference wizard of Silverlight is very powerful and it will automatically implement the INotifyPropertyChanged interfaces and ObservableCollection classes when generating code for the datacontracts. The generic ObservableCollection<T> class implements an INotifyCollectionChanged interface and this enables a collection to notify bound UI controls when items are added or removed. So maybe it is better to keep your entity classes in your domain layer and not share them with a Silverlight project.
An alternative solution to share and reuse methods is to implement extension methods. The file which contains the extension methods can be shared between your full .NET domain layer and a Silverlight assembly. This works fine but of course it also has several limitations.
namespace ScipBe.Demo.Domain
{
public static class EmployeeExtensionMethods
{
public static string GetFullName(this EmployeePoco emp)
{
return emp.FirstName + " " + emp.LastName;
}
}
}
Sharing logic in your Entity Framework entities between your .NET domain layer and Silverlight
In my opinion the best solution to share logic of your entities is to use partial classes. You only share some extra logic and not all the properties. I tried it with POCO classes and ADO.NET Entity Framework entities and it should also work fine with other ORM tools like LINQ to SQL, NHibernate, LLBLGen, ...
In this example I will demonstrate how to share an entity of the EDM (Entity Data Model) in a Silverlight application. Just use the EDM to shape your entities. The Entity Framework will generate code for your entities which will contain Serializable, DataContract and DataMember attributes. This EDMX file and the code behind Design.cs file will only be used in the full .NET domain layer.
All extra logic (FullName, ToString, Validate, IsAutoCompleteSuggestion) has to be implemented in a partial class in a seperate file. So far this is the standard approach you would follow with the Entity Framework.
There are 2 very important issues:
- You have to link the file with the partial class in the same Silverlight project which will contain the service reference.
- Furthermore the name of the namespace of the partial class file should be the one which is used in the code generated by the Add Service Reference wizard. Use the SILVERLIGHT compiler directive to denote the differences.
On the Silverlight client you have to use the Add Service Reference wizard to generate classes for the datacontracts. The Add Service Reference wizard will automatically implement the INotifyPropertyChanged interfaces and ObservableCollection classes and as a result two-way databinding can be used with your entities. Because the partial class with the methods is shared, you are also able to call these methods in the Silverlight client.
Accessing properties in the partial class works fine in both layers, but there is a small issue when you want to use private members. The Entity Framework generates private members and their names start with an underscore (e.g. _FirstName). By contrast the Add Service Reference wizard generates names which end with the word Field (e.g. FirstNameField).
It is a pity that the different Microsoft teams use different naming conventions for the private members. Probably this issue can be solved in the next version of the Entity Framework which will support T4 templates. The ADO.NET team already confirmed that the EntityClassGenerator in Visual Studio 2010 will use these T4 templates and that developers will be able to modify them.
Until then you need to add compiler directives to access private members in both layers. This is how the partial class would look like:
#if SILVERLIGHT
namespace ScipBe.Demo.Silverlight.EmployeeServiceReference
#else
namespace ScipBe.Demo.Domain
#endif
{
public partial class Employee
{
public string FullName
{
get
{
#if SILVERLIGHT
return this.FirstNameField + " " + this.LastNameField;
#else
return this._FirstName + " " + this._LastName;
#endif
//return FirstName + " " + LastName;
}
}
public override string ToString()
{
return FullName;
}
public bool Validate()
{
return ((EmployeeId > 0)
&& (!string.IsNullOrEmpty(FirstName))
&& (!string.IsNullOrEmpty(LastName)));
}
public static bool IsAutoCompleteSuggestion(string search, object item)
{
Employee emp = item as Employee;
if (emp == null)
return false;
search = search.ToUpper();
return (emp.EmployeeId.ToString().Contains(search) ||
emp.FirstName.ToUpper().Contains(search) ||
emp.LastName.ToUpper().Contains(search));
}
}
}
Sharing common logic and separating framework dependent implementations
Sometimes you want to share a common interface and some common logic but the implementation of some methods is different depending if you are using the class on the client or on the server. e.g. When you have a Logger class then logging in Silverlight could be done in memory or with isolated storage. On server side the Logger will propably use the Enterprise Library or Log4Net. You could choice to share your full Logger class and add SILVERLIGHT compiler directives to denote the differences but it there is a better approach. In cases where the method implementations are really different, partial methods are the way to go.
Implement the interface and all common logic in a partial class in one file (e.g. Logger.cs). Only declare the partial methods in this file (WriteToLog).
namespace ScipBe.Demo.Framework
{
public enum EventType
{
Error,
Warning,
Information
} ;
public class LogData
{
public string Message { get; set; }
public EventType EventType { get; set; }
}
public partial class Logger
{
public void Write(string message)
{
WriteToLog(new LogData() {Message = message});
}
public void Write(string message, EventType eventType)
{
WriteToLog(new LogData() {Message = message, EventType = eventType});
}
partial void WriteToLog(LogData logdata);
}
}
In a second file (e.g. LoggerServer.cs) you can write the partial methods with their server side implemention.
namespace ScipBe.Demo.Framework
{
public partial class Logger
{
partial void WriteToLog(LogData logdata)
{
// Full .NET implementation
// Enterprise Library, Log4Net, Write to file, Write to database, ...
}
}
}
Now you can share/link the Logger.cs file in your Silverlight project. Proceed by adding a new file (e.g. LoggerClient.cs) and write the Silverlight implementation of this partial method.
namespace ScipBe.Demo.Framework
{
public partial class Logger
{
partial void WriteToLog(LogData logdata)
{
// Silverlight implementation
// Store in memory, Write to isolated storage, ...
}
}
}
I hope you like this small tutorial which explains some code sharing techniques with Silverlight. If you have any questions, suggestions or comments, be sure to let me know.