Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Logging in Silverlight and WP7 with MVVM Light

0.00/5 (No votes)
10 Feb 2011 1  
Logging in Silverlight and WP7 with MVVM Light

Most of the projects that I am involved in have some need of logging various kinds of data to the server. This ranges from logins to user activity to error reporting. Regardless of the need, it always seems to show up in some form. Now I know what you are thinking, “if you run into this need all of the time, why not create an extensible solution that you can reuse?” My thoughts exactly, so let’s see if we can build one.

One of the initial requirements that I have is to use some sort of messaging to report log messages on the client. This is done primarily because we create a lot of segregated projects using things like MEF. Using messaging will allow us to centralize the handling of the logging and make our life a little easier. To handle this aspect, I am using MVVM Light by GalaSoft. While this will not be an MVVM project in itself, the components will nicely integrate into any of your MVVM projects. If you want an introduction to messaging with MVVM Light, you can check out an earlier blog post here.

The other plus to this solution and using MVVM Light, since it has a WP7 version, is that it will work just as well on your WP7 projects. To make sure this works with little to no modifications, we will stick to WCF services instead of WCF RIA services.

Before we get too deep in the development of our solution, let’s take a quick look at the components we will need to develop.

  • Database
  • Web service to handle data access
  • Client library to handle the data on the Silverlight side
  • Demo code to test out our newly built solution

So let’s go ahead and get started, shall we…

Solution Setup

Before we begin, I wanted to take a quick moment to walk through the setup of the project. To begin with, let's set up a standard Silverlight application.

image

Reminder: We won’t be using RIA for this project.

To start out, let’s get our server side setup first.

The first thing we need to do is build up our database. Now you can do this in a variety of different ways, but since this is a demo, we will use SQL Express.

Let’s add an App_Data folder to our solution. If you are unfamiliar with doing this, right click the web-project, then select: Add ASP.NET Folder –> App_Data.

image

In our App_Data folder, we can add a SQL Server Database. For our example, let’s give it a very unique name: Logging.mdf. If you double-click the database, the Server Explorer will open. Add a new table to our database by right-clicking the Tables node and select Add New Table.

The table structure for the our new table, LogData, should look like this:

image

Not the most complicated structure in the world, but it is extensible through the MetaData column, which we will get into shortly.

And there you have it. Our application is setup and ready to start building our solution on.

Building Our Web Service

Now that we have a structure to work with, let’s get started on our web service. We have an extra step or two to build on top of the extensibility feature.

The first thing we need is to have access to our database. For the purposes of this demo, we will be using the ADO.NET Entity Data Model.

Before we get started adding code, I am putting all of this web service code located in Logging folder that I added to the project.

Let’s get started by adding a new ADO.NET Entity Data Model: LoggingDB.edmx. When the wizard launches, select the “Generate from Database”. The second screen should be populated for you with the database that we added and a default connection string name. If you are not used to this wizard, here is a screenshot of what the last page of the wizard will look like:

image

 

Notice that neither the “Pluralize” nor the “foreign key” items are checked. Since we have a single table, there is no need for foreign keys. As far as the “pluralize” option, our LogData table will look funny being called LogDatas if we select this.

Now that we have a way to access our data, there are a few classes that need to be developed.

The first is a utility class that will handle the serialization of our log metadata class. It's a handy little generic class to keep around.

public class GenericSerializer<T>
 {
     public T Deserialize(string objData)
     {
         var xmlSer = new XmlSerializer(typeof(T));
         var reader = new StringReader(objData);

         return (T)xmlSer.Deserialize(reader);

     }

     public string Serialize(T obj)
     {
         var xmlSer = new XmlSerializer(typeof(T));
         var writer = new StringWriter();

         xmlSer.Serialize(writer, obj);
         return writer.ToString();

     }
 }

The next class we need to create is our LogMetadata class. This is where we can extend our logging structure. This class makes it possible since we serialize the class and store the data in the database as XML. So as long as the class can be serialized, we can extend our logging structure to support any information we need and keep the same database. This is extremely handy when you want to store different types of log information in the same table.

For demonstration purposes, our class will only contain a single property. However, you can see where you can quickly expand to store information like IP address, user information, etc.

[Serializable]
[DataContract]
public class LogEntryMetadata
{
    [DataMember]
    public string Category { get; set; }
}

Now we can tackle the main class for the application, the LogEntry class. The structure of the class mimics our database, except we are storing the LogMetadata as an object instead of the XML the database contains.

[DataContract]
public class LogEntry
{
    [DataMember]
    public Guid ID { get; set; }

    [DataMember]
    public DateTime DTStamp { get; set; }

    [DataMember]
    public LogEntryMetadata Metadata { get; set; }

    [DataMember]
    public string Data { get; set; }
}

 

With our data classes ready to go, the only thing left is to create our web service. To do this, we will add a Silverlight-enabled WCF Service to our project, Logging Service.

image

The structure of our service is rather straight forward and I only have 2 operations for this demo. Of course, you can build on this, but this will get us started.

The first operation allows us to add an entry into our database. The thing to take notice of in this class is where we serialize the LogMetadata class to store it in the database.

The second operation is simply a Get operation that is sorting the date by date/time. Again, you will see where we are deserializing the LogMetadata class to build up our LogEntry classes.

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode =
    AspNetCompatibilityRequirementsMode.Allowed)]
public class LoggingService
{
    private readonly LoggingEntities _context = new LoggingEntities();
    private readonly GenericSerializer<LogEntryMetadata> _serializer =
        new GenericSerializer<LogEntryMetadata>();

    [OperationContract]
    public bool AddLogEntry(LogEntry value)
    {
        try
        {
            LogData data = _context.LogData.CreateObject();
            data.DTStamp = DateTime.Now;
            data.Data = value.Data;
            data.ID = value.ID == Guid.Empty ?
                                Guid.NewGuid() :
                                value.ID;
            data.MetaData = _serializer.Serialize(value.Metadata);

            _context.LogData.AddObject(data);
            _context.SaveChanges();
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    [OperationContract]
    public IEnumerable<LogEntry> GetLogEntries()
    {

        return from l in _context.LogData.AsEnumerable()
                  orderby l.DTStamp
               select new LogEntry
                          {
                   ID = l.ID,
                   Data = l.Data,
                   DTStamp = l.DTStamp,
                   Metadata = _serializer.Deserialize(l.MetaData)
               };
    }
}

And that is all there is to our service.

NOTE: Please compile the application at this point BEFORE going any further. It is important that your new service is compiled before adding to the Silverlight application.

The Silverlight Client

In order to build our Silverlight client, we are going to break it up into 2 parts: the main application and the logging client. We will start with the logging client first.

To separate this out, let’s start by adding a new Silverlight Class library to our solution, LoggingClient.

The only additional reference that we need to add to this library is the MVVMLight library (GalaSoft.MvvmLight.SL4 for this app). If you haven’t downloaded the libraries yet, you can get them off of the CodePlex site http://mvvmlight.codeplex.com/.

The next item we need to add is our WCF service. Prior to doing this, we need to add a name to our endpoint in the web.config, LoggingCustom.

<service name="MVVMLoggingDemo.Web.Logging.LoggingService">
  <endpoint name="LoggingCustom" 

  address="" binding="customBinding"

     bindingConfiguration="MVVMLoggingDemo.Web.Logging.LoggingService.customBinding0"

     contract="MVVMLoggingDemo.Web.Logging.LoggingService" />
   <endpoint address="mex" binding="mexHttpBinding" 

   contract="IMetadataExchange" />
</service>

You can get a detailed reason for this from my last entry: Deploying Silverlight with WCF Services.

Back in our new class library, right-click the project and “Add Service Reference”. If you select the “Discover” button, your wizard should look something like this:

image

In order to take advantage of the MVVM Light messaging, we are going to be sending two different types of messages. The first one is going to be an action message. The intent of this message is simply to let registered components know that some action needs to happen or has taken place. In our example, we will also add a data section so we can send any necessary data along with the action. To handle the action type, it is a good practice to include an enum, instead of doing something like adding a string. It helps to prevent typo errors.

public enum LogMessageAction
{
    AddLogEntryCompleted,
    GetLogData,
    GetLogDataCompleted
}

public class LogMessage
{
    public LogMessageAction Action { get; set; }
    public object Data { get; set; }
}

The class we create to handle the logging on the client side has two real responsibilities: communicating with our WCF service and communicating with the application via messaging. The WCF service communication is rather straight forward. In our example, we only have the two operations. The logging class wraps around those two operations and ties them to the appropriate messages. Let's take a look at our logging class.

public class Logger
{
    private readonly LoggingServiceClient _client;
    public Logger()
    {
        // Initialize the service client
        var uri = new Uri("../Logging/LoggingService.svc", UriKind.Relative);
        var addr = new EndpointAddress(uri.ToString());
        _client = new LoggingServiceClient("LoggingCustom", addr);

        // Register event completions
        _client.AddLogEntryCompleted += new
            EventHandler<AddLogEntryCompletedEventArgs>(_client_AddLogEntryCompleted);
        _client.GetLogEntriesCompleted += new
            EventHandler<GetLogEntriesCompletedEventArgs>(_client_GetLogEntriesCompleted);

        // Register to receive all messages of type LogEntry
        Messenger.Default.Register<LogEntry>(this, ReceiveLogEntry);

        // Register to receive log messages
        Messenger.Default.Register<LogMessage>(this, ReceiveLogMessage);
    }

    void _client_GetLogEntriesCompleted(object sender, GetLogEntriesCompletedEventArgs e)
    {
        Messenger.Default.Send(new LogMessage
                                { Action = LogMessageAction.GetLogDataCompleted,
                                  Data = e.Result
                                });

    }

    void _client_AddLogEntryCompleted(object sender, AddLogEntryCompletedEventArgs e)
    {
        Messenger.Default.Send(new LogMessage
                                { Action = LogMessageAction.AddLogEntryCompleted
                                });
    }

    public void ReceiveLogMessage(LogMessage msg)
    {
        switch(msg.Action)
        {
            case LogMessageAction.GetLogData:
                _client.GetLogEntriesAsync();
                break;
        }
    }
    public void ReceiveLogEntry(LogEntry entry)
    {
        // Call service AddLogEntry operation
        _client.AddLogEntryAsync(entry);
    }
}

The constructor of our class simply handles three items:

  • Initializes our WCF service client
  • Adds event handles to the completed events of our WCF service operations
  • Registers to receive 2 types of messages (our action message and log entries)

Each of the event handlers simply send an action message to the system informing that an operation has been completed. The GetLogEntriesCompleted handler also includes the log entries in the Data property of the message.

The message handlers both call the appropriate WCF service operation. Notice that the ReceiveLogMessage is using a switch statement to filter out only the message types that it is looking for.

Now we have a logging service that we can use from anywhere in our application to log information back at the server. The only thing we have left to do is to create an instance of the logger class somewhere. You can handle this most places, however, I typically need logging at the very beginning of the life cycle, so I tend to add the class to my App class of the application.

In order to do this, we need to add 2 references to our main Silverlight application: one to the Logging.Client project we created and the other to the same MvvmLight library.

There is one additional item that we need to handle in order for our WCF service to work. You will notice that the Logging.Client library had a ServicReferences.ClientConfig file added to it. Your main Silverlight application will also need this file. You can either add an identical file or add the file in as a link, but you will need one in the main application. Note: There are other ways around this (such as defining the binding via code), but this will work for our purposes.

Once that is completed, simply add an instance of the Logger class to your App class.

private Logger _logger;

public App()
{
    this.Startup += this.Application_Startup;
    this.Exit += this.Application_Exit;
    this.UnhandledException += this.Application_UnhandledException;

    _logger = new Logger();
    InitializeComponent();
}

And there you have it. Instant logging in your Silverlight application. So what’s next, let’s build a small UI to test it.

Testing Our Logging Structure

To test our new logging, I want to build a UI that simply has two parts: a UI to add logs and a UI to display the logs in the system.

image

Maybe not the most elaborate UI in the world, but it will get the job done. Here is what the XAML for our little UI looks like:

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="280" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Grid Grid.Column="0" Margin="10,0,10,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Text="Log Entry"/>
        <Grid Grid.Row="2" VerticalAlignment="Top"
              Height="Auto" Margin="10"
              Background="#FFF0F0F0">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Category :"/>
            <ComboBox x:Name="cbCategory"
                      Grid.Column="1"
                      ItemsSource="{Binding Categories}"/>
            <TextBlock Text="Data :" Grid.Row="1"/>
            <TextBox x:Name="txtData" Grid.Column="1"
                     Grid.Row="1" Height="75"/>
            <Button x:Name="btnSubmit" Content="Submit"
                    Grid.Row="2" Grid.ColumnSpan="2"
                    HorizontalAlignment="Center"
                    Click="btnSubmit_Click"/>
        </Grid>
    </Grid>
    <Grid Grid.Column="1" Margin="10,0,10,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Text="Log Data"/>
        <ListBox x:Name="lbData"
                 Grid.Row="1" Width="300"
                 HorizontalAlignment="Left">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Metadata.Category}"/>
                        <TextBlock Text="{Binding DTStamp}"
                                   HorizontalAlignment="Right"
                                   Grid.Column="1"/>
                        <TextBlock Text="{Binding Data}"
                                   Grid.Row="1"
                                   Grid.ColumnSpan="2"
                                   TextWrapping="Wrap"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Grid>

Our code behind for this UI is also rather straight forward. Remember, this is a testing UI, please don’t try this at home. :)

The first thing we do is to query the log database when the application is loaded. We also repeat this anytime you submit a log entry. The last item is to handle the button click to submit an entry to be logged. That is about all we have here.

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();

        cbCategory.ItemsSource = new List<string>() {"Message", "Alert", "Action Item"};
        Messenger.Default.Register<LogMessage>(this, ReceiveLogMessage);
        GetLogData();
    }

    private void GetLogData()
    {
        Messenger.Default.Send(new LogMessage
        { Action = LogMessageAction.GetLogData});

    }
    private void ReceiveLogMessage(LogMessage msg)
    {
        switch(msg.Action)
        {
            case LogMessageAction.AddLogEntryCompleted:
                MessageBox.Show("Added log to database.");
                GetLogData();
                break;
            case LogMessageAction.GetLogDataCompleted:
                lbData.ItemsSource = (IEnumerable)msg.Data;
                break;
        }
    }
    private void btnSubmit_Click(object sender, RoutedEventArgs e)
    {
        LogEntry msg = new LogEntry
                           {
                              Data = txtData.Text,
                              Metadata = new LogEntryMetadata
                        {
                            Category = (string)cbCategory.SelectedItem
                        }
                           };
        Messenger.Default.Send(msg);
    }
}

You should now have a UI that you can test the logging features for. Once you have done that, you should be able to reuse your logging class and extend it to meet your project needs.

Please feel free to let me know if you run into any issues or have any ideas on making a better design.

You can download the code for this project here.

Now don’t think I forgot about all of you WP7 folks. This same system works on WP7 with a few modifications. Part II (which is coming in a couple of days) will do a WP7 walk-through to demonstrate how it works on WP7. So stay tuned…

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here