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.
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.
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:
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:
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.
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:
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()
{
var uri = new Uri("../Logging/LoggingService.svc", UriKind.Relative);
var addr = new EndpointAddress(uri.ToString());
_client = new LoggingServiceClient("LoggingCustom", addr);
_client.AddLogEntryCompleted += new
EventHandler<AddLogEntryCompletedEventArgs>(_client_AddLogEntryCompleted);
_client.GetLogEntriesCompleted += new
EventHandler<GetLogEntriesCompletedEventArgs>(_client_GetLogEntriesCompleted);
Messenger.Default.Register<LogEntry>(this, ReceiveLogEntry);
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)
{
_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.
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…