Click here to Skip to main content
15,881,882 members
Articles / Desktop Programming / WPF

Earth Quake - A Composite WPF disaster monitoring application

Rate me:
Please Sign up or sign in to vote.
4.95/5 (20 votes)
28 May 2012CPOL16 min read 62.5K   3K   58   19
A composite (Prism 4) WPF application using Bing Maps to display earthquakes from around the world.

Earth Quake

Introduction

Earth Quake is a composite WPF application designed to display all recently reported earthquakes from around the world using Bing Maps.

The application is targeted at the .NET 4 runtime with Prism 4 as its composite framework. The code examples and solution for Earth Quake are in VB.NET and the solution is for Visual Studio 2010.

The article assumes the reader has some knowledge of WPF and that may be aware of Prism 4, MEF, and MVVM. This is not an article on MEF or MVVM, however there are some brief explanations to help cover the topics.

Earth Quake has been created help illustrate the following:

  • Prism composite application development
    • Regions
    • View discovery
    • Communication through Event Aggregation and Commanding
  • MVVM
  • MEF
  • Bing Maps
  • WPF and data binding 

Background

For me, WPF, Prism, MEF and MVVM are just pure pleasure to work with.

There are many fantastic articles out there that describe in great detail how to develop applications using these technologies and patterns. I do not want to detract from these so for that reason, see the links below to some resources that should help if you are unfamiliar with them.

I wanted to design a small and basic application that covered some features of Prism and WPF and hope that you will find it helpful or even useful. This article is one of a couple that I hope to bring to the table.

It is also worth noting (and something I thought I added but did not) was that whilst developing this application I found a very similar application that was already developed with the same concept. This application was a massive help to me and was developed by Brian Lagunas. Originally I was using a static image with dynamically placed points however Bing Maps did provide a better alternative. The example is found here!  

Table of Contents

References

Earth Quake has a couple of referenced libraries that have been included with the project download. The references are needed in order for you to run the application.

Name Path Assemblies Description
Prism 4 \EarthQuake\Libs\Prism 4\ Microsoft.Practices.Prism.dll
Microsoft.Practices.Prism.Interactivity.dll
Microsoft.Practices.Prism.MefExtensions.dll
Prism 4 composite framework
Bing Maps For WPF \EarthQuake\Libs\Bing Maps\ Microsoft.Maps.MapControl.WPF.dll Bing Maps WPF control
Json.Net \EarthQuake\Libs\Json.Net\ Newtonsoft.Json.dll JSON framework for .NET

In order to use MEF you will need to import the following system dll (MEF is part of the .NET 4 runtime):

System.ComponentModel.Composition

The Maps module contains references to the Microsoft.Expression.Interactions DLL. This is for executing commands on non-commanding objects. If you have expression studio 4 (Blend) you will be ok. If not then I would recommend either installing Blend or downloading the Blend SDK from here.:

Microsoft.Expression.Interactions

Bing Maps

Earth Quake uses the WPF control from the Bing Maps team that has recently been released. Bing Maps had an API for AJAX etc but now there is a WPF control. The control assembly is provided with the solution download however you many want to get the SDK from here.

Bing Maps API Key

Although not necessary to develop with, a Bing Maps API key should be created from the Bing Maps Portal. If not you will see a box over the map stating you should get a key as shown below:

Missing Bing Maps API Key

The portal also has links to some resources as well as the other SDK downloads.

Once you have generated / registered a key you will need this later as the Bing Maps control will require the key credential in order to remove the message.

Json.Net

Earth Quake downloads the reported quake events from the United States Geological Survey website. This site has a host of feeds which vary in download format (CSV, ATOM, etc.) and frequency of the registered earthquake. Earth Quake downloads a 30 day feed in the Json format. For that reason I wanted to see if there was a .NET framework for JSON that I could execute lambda expressions on.

Json.Net (by James Newton-King) is one such framework. It provides a mechanism to query a JSON feed with ease. Json.Net provides these features (taken from the website):

  • Flexible JSON serializer for converting between .NET objects and JSON
  • LINQ to JSON for manually reading and writing JSON
  • High performance, faster than .NET's built-in JSON serializers
  • Write indented, easy to read JSON
  • Convert JSON to and from XML
  • Supports .NET 2, .NET 3.5, .NET 4, Silverlight, and Windows Phone

A brief example of how to use the framework is shown below:

VB
' A demo Json feed. This would be a webclient
' download from a remote location but is a string for a quick demo.
Dim feed As String = "{""Name"": ""Apple"",""Expiry"": 1230422400000,""Price"": 3.99,
                         ""Sizes"": [""Small"",""Medium"",""Large""]}"

' Parse the feed using the Json.Net parser
Dim o As JObject = JObject.Parse(feed)

' Get the name of the apple
Dim name As String = DirectCast(o("Name"), String)

' Get the array of sizes using the JArray object
Dim sizes As JArray = DirectCast(o("Sizes"), JArray)

' Just get the first size in the sizes collection. Collections are zero-index based.
Dim smallest As String = DirectCast(sizes(0), String)

Prism 4

If you have been developing on Silverlight, WPF, or even WP7 for some time you may have encountered the phrase "Composite Application". Prism (Formally Composite Application Guidance for WPF and Silverlight), from the Microsoft Patterns and Practices team was created to allow developers to easily create modular and decoupled applications.

Using design patterns that embody important architectural design principles, such as separation of concerns and loose coupling, Prism helps you to design and build applications using loosely coupled components that can evolve independently but which can be easily and seamlessly integrated into the overall application.

Earth Quake uses the Prism 4 framework to give you an example of modularization. To download the framework and for more information please see the links below.

Earthquakes

As mentioned above, Earth Quake obtains reported quake information from the United States Geological Survey website. Each feed is broken down by the number of days since the event happened, the strength of the quake and the format the information can be read from.

The formats available are:

  • CSV
  • ATOM
  • RSS
    • Contains limited information compared to ATOM and JSON.
  • JSON
  • JSON(P)
    • The same format as JSON however this data feed is wrapped inside a function call, eqfeed_callback.  Earth Quake uses the standard Json feed.
  • KML
    • Google earths xml format for geographic apps.
  • Twitter
  • Email

Architecture

Solution

Earth Quake is a basic composite application. The solution is broken down into several different projects all providing unique functionality to the overall application. The layout of the solution is shown below:

Solution Layout

  1. Business. The shared project under the business solution folder is for the majority of all shared business logic and domain classes. These objects are unlikely to be shared between all modules as not all modules will require it. The majority of Prism applications have the Infrastructure project as their globally shared project however I like to keep things a little lighter as this could become a very heavy project in its own right. It is more likely that you would have shared (core) projects for each module in a larger application.
  2. Core. These core projects are likely to be referenced by the majority of your modules (if not all of them).
    1. Infrastructure: The infrastructure project has elements such as composite commands, event payloads and interfaces. This project is very important and will likely be referenced by all modules.
    2. Resources: Slightly less important compared with the infrastructure project however, this project houses the elements such as themes, image resources and other "application specific" styling.
  3. Modules. The modules are the projects that make up the total functionality of the overall application. Each module can have a direct dependency to the infrastructure project (and likely will). Modules may not be part of the overall application solution as they could be developed offsite for example. The beauty of having applications developed in a modular way is having the opportunity to focus development teams on individual aspects of the application without stepping on toes.
  4. Shell. This is the shell or module host that is used to house and render views and implement logic from modules and other dependencies. The shell could be a WPF or Silverlight application for example.

MVVM

The MVVM Pattern
Source: Microsoft

Earth Quake uses the MVVM pattern throughout. There are many great MVVM frameworks out there (MVVM light, Cinch, Jounce to name but a few) but I wanted to keep the demo simple enough so that it just covers a couple of topics. Only elements from the Prism framework and Josh Smith's ViewModelBase class are used. Again this is not a tutorial on MVVM as I think this has been covered in better detail by others.

MEF

Earth Quake uses MEF (Managed Extensibility Framework) to resolve views and viewmodels. Each view will use property injection to wire up the views dependency on it's related viewmodel. Earth Quake also uses constructor injection in other parts of the application.

Basically (and I mean basically) each view and viewmodel have an export class attribute. With this attribute MEF uses this to help resolve the object. If you have a property (or constructor) where you want the object to be placed you would simply declare an import attribute. This is shown below:

The Shell viewmodel. Notice the "Export" attribute.

VB
''' <summary>
''' The Shell ViewModel
''' </summary>
''' <remarks></remarks>
<Export(GetType(ShellViewModel))> _
<PartCreationPolicy(NonShared)> _
Public Class ShellViewModel
Inherits 
ViewModelBase
    .....
End Class

The shell view will get the viewmodel via property injection. The "Exported" viewmodel will be "Imported" into the shell. Notice the "Import" attribute on the ViewModel property. This is shown below:

VB
''' <summary>
''' Sets the ViewModel.
''' </summary>
''' <remarks>
''' This set-only property is annotated with the <see cref="ImportAttribute"/> 
so it is injected by MEF with
''' the appropriate view model.
'''</remarks>
<Import()> _
Private WriteOnly Property ViewModel() As 
ShellViewModel
Set(ByVal value As ShellViewModel)
Me.DataContext = value
End Set
End Property

For those that are unfamiliar with MEF, MEF is a Microsoft framework for resolving objects. MEF is sometimes referred to as a DI / IoC container however this is not really what its purpose is for but does have many similarities. Prism also supports the Unity framework which is the DI / IoC framework from Microsoft however this is not covered in this article.

Earth Quake

OK, now the application. Earth Quake, in its current form, is a simple user interface with one main view; the map. Other aspects of the UI relate to control of the actual map and the status of the application.

UI Overview

UI Overview

The application is broken down into three regions:

  • R1: The toolbar region.
  • R2: The main map region.
  • R3: The status bar region.

The application also has 3 main functions:

  1. Navigation - Zoom, centre and position.
  2. Tools - Change map style.
  3. Details - The earthquake information triggered via a pin mouse over.

The basic concept is that you are free to pan and move around the globe and view any earthquake points that maybe on screen. Every time the user moves the mouse over a map pin, the earthquake information is displayed in the Quake details window.

Shell

The shell is the core application that can host the modules associated with the application. The shell can be a Silverlight or WPF application. In this instance the Shell project is a WPF .NET 4 application.

At present there is only one view and it's related ViewModel. This is an important view as it is the core layout (similar to a masterpage in ASP.NET) view containing regions that module views would be injected into. For a view / shell to contain regions it is recommended that the following namespaces be imported:

XML
xmlns:inf="clr-namespace:EarthQuake.Infrastructure;assembly=EarthQuake.Infrastructure"
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism"

The "prism" namespace is the reference to the Microsoft.Practicies.Prism library. This assembly is responsible for all region management. The "inf" namespace is commonly a reference to the solution infrastructure project. Although not mandatory, the reason for the reference is to use strongly typed region and module names from the "WellKnownModuleNames" and "WellKnownRegionNames" found in the infrastructure project.

The shell view (called shell.xaml) is initiated via the bootstrapper. The shell view contains the following regions which will be injected into:

XML
<!-- ToolBars region -->
<ItemsControl Grid.Row="1" prism:RegionManager.RegionName="{x:Static inf:WellKnownRegionNames.ToolBarRegion}" />

<!-- Map Region -->
<ContentControl Grid.Row="2" prism:RegionManager.RegionName="{x:Static inf:WellKnownRegionNames.MapRegion}" />

<!-- Status Region -->
<ContentControl Grid.Row="3" prism:RegionManager.RegionName="{x:Static inf:WellKnownRegionNames.StatusRegion}" />

There are two types of region containers used here. The ContentControl and the ItemsControl. Basically the ContentControl is used for displaying a single view or UI element. The ItemsControl is for multiple items such as buttons. You will notice that the RegionManager.RegionName property is set to the strongly typed name from the WellKnownModuleNames as explained above.

Bootstrapper

The other aspect of the shell is to provide a means to specify the modules using a module catalogue and to configure the region adapters or behaviors. Earth Quake inherits from the MEF Bootstrapper which comes from the Microsoft.Practices.Prism.MefExtensions assembly. The Bootstrapper creates and initializes the Shell window and configures the modules associated with the project. Note that the bootstrapper class should be at the root level of the project.

The ConfigureAggregateCatalog() from the Bootstrapper is the method responsible for creating references to the modules. Currently all modules are referenced in code but you could have a module directory to monitor or xml configuration file to use. More common than not is to reference the modules in code however I find that the directory method does provide a more "loosely coupled" scenario.

VB
''' <summary>
''' Configures the aggregate catalog.
''' </summary>
''' <remarks></remarks>
Protected Overrides Sub ConfigureAggregateCatalog()
MyBase.ConfigureAggregateCatalog()

'register the modules
AggregateCatalog.Catalogs.Add(New AssemblyCatalog(GetType(Bootstrapper).Assembly))
AggregateCatalog.Catalogs.Add(New AssemblyCatalog(GetType(StatusModule.StatusModule).Assembly))
AggregateCatalog.Catalogs.Add(New AssemblyCatalog(GetType(MapModule.MapModule).Assembly))
AggregateCatalog.Catalogs.Add(New AssemblyCatalog(GetType(ToolBarModule.ToolBarModule).Assembly))
End Sub

In order for the application to initialize the bootstrapper and make the composite application come alive, the bootstrapper needs to be instantiate in the OnStartup event. This normally done in the Application.xaml code behind.

VB
Protected Overrides Sub OnStartup(ByVal e As StartupEventArgs)
MyBase.OnStartup(e)

AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf AppDomainUnhandledException

Me.ShutdownMode = ShutdownMode.OnMainWindowClose

Try
Dim bootstrapper As New Bootstrapper()
bootstrapper.Run()
Catch ex As Exception
HandleException(ex)
End Try
End Sub

Infrastructure

The Infrastructure is the core application assembly. This should and will contain all core functionality to be shared across each module. This functionality will be things like interfaces, event payloads, composite global commands and other key objects.

Earth Quake does use an infrastructure project to contain these important aspects of functionality however there is another slightly less important shared library used to house all domain objects that are less important to the overall application as not all modules will reference it. This is to keep things a little lighter as the infrastructure project could in its own right become too heavy. You will more likely in larger application, see modules with individual core / shared assemblies that house module dependent objects but this is not the case with Earth Quake.

Modules

There are three modules that exist as part of the application which are found in the solution folder "Modules". Modules can be part of the overall solution or from satellite assemblies either developed offsite or from a third party (being an extensible add-in). In order for a module to exist there must be a class that implements the IModule (member of Microsoft.Practices.Prism.Modularity) interface. This class will be responsible for registering the module views with the application regions.

Each module initiation class (a class that implements the IModule interface) has a method called Initialize that is called when the module is instantiated in the application bootstrapper. For each module in Earth Quake, the views within them are registered with a region via the region manager. To register a view using a term called view discovery, the method RegisterViewWithRegion is used. View discovery is a pattern that will ensure the view is added to a region when the region is added automatically. View Injection on the other hand is where a view is added (Injected) into a region which is generally done manually and does require the region to be present for obvious reasons.

The two examples below show how this could be achieved.

VB
'View discovery
_regionManager.RegisterViewWithRegion(WellKnownRegionNames.MapRegion, GetType(WorldMap))

'View injection
_regionManager.Regions(WellKnownRegionNames.MapRegion).Add(GetType(Map))

Toolbar

The toolbar module is pretty simple. It is a basic primitive toolbar control with two buttons on it; Refresh and Exit. These buttons use two different mechanisms to achieve an event. Refresh uses the event aggregator to raise an event to event subscribers which could be in multiple modules. The Exit button uses a composite command to trigger any subscribed delegate commands. In this instance, the exit button triggers the ExitApplicationCommand (from the EarthQuake.Infrastructure.Commands.Global namespace). But how does it actually exit the application?. Buried in the shell ViewModel is a delegate command that has been registered to the global composite command ExitApplicationCommand. Any delegate commands that are subscribed to the global composite command will receive the event notification and will action any delegate method associated with that command.

Status Bar

The status bar is again a fairly simple statusbar control. The status bar relies heavily on event aggregation for displaying the map centre location stating the latitude and longitude. The status bar also gets a status message in the same way.

Map

The map module is the main module and does the majority of the work. The module contains a view (map.xaml), a viewmodel (MapViewModel), and a model (MapModel).

Setting Bing Maps

To setup Bing Maps you need to reference the Microsoft.Maps.MapControl.WPF.dll. You then need to add the reference in the view.

XML
xmlns:m="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"

Because the Map pins use the Microsoft.Expression.Interactions assembly for triggering commands via mouse enter and mouse leave, you will need to reference this assembly in your project and then reference it in the markup.

XML
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Once you have a reference to the map control, you can then add the control to the markup.

XML
<m:Map Name="WorldMap"
	AnimationLevel="Full"
	Center="{Binding MapCentreLocation, Mode=TwoWay}"
	CredentialsProvider="Your map API Key here"
	Mode="{Binding MapMode}"
	ZoomLevel="{Binding ElementName=Zoom, Path=Value, Mode=TwoWay}">
	<m:MapItemsControl ItemsSource="{Binding EarthquakeLocations}" 
	    ItemTemplate="{StaticResource EarthquakeLocationsTemplate}" />
</m:Map>

The map has a few properties on it which are bound to the viewmodel or bound to a few other controls on the page. Note that the property "CredentialsProvider" should be where the API key is added. Again for development purposes this is not required however I would get one.

In order to display map pins you will need to set the "MapItemsControl". You will notice that the MapItemsControl has been bound to the EarthquakeLocations ObservableCollection property in the viewmodel. I have set an ItemTemplate for the pins as shown here:

XML
<DataTemplate x:Key="EarthquakeLocationsTemplate">
	<m:Pushpin Name="Pin"
		m:MapLayer.Position="{Binding Location}"
		Tag="{Binding}"
		ToolTip="{Binding Title}">
	<i:Interaction.Triggers>
		<i:EventTrigger EventName="MouseEnter">
			<i:InvokeCommandAction Command="{Binding ElementName=WorldMapView, Path=DataContext.PinMouseOverCommand}" CommandParameter="{Binding}" />
		</i:EventTrigger>
		<i:EventTrigger EventName="MouseLeave">
			<i:InvokeCommandAction Command="{Binding ElementName=WorldMapView, Path=DataContext.PinMouseOverCommand}" CommandParameter="" />
		</i:EventTrigger>
	</i:Interaction.Triggers>
	</m:Pushpin>
</DataTemplate>

You will notice the alias "i" used for the event triggers. This alias is to the Expression interactivity namespace. In order to pop open the Earthquake details WPF popup control I used the interactions to trigger and invoke the delegate command "PinMouseOverCommand" in the viewmodel. Basically this just opens and closes the details window depending on your mouse position.

Getting the Map Data

As mentioned at the start of the article, the earthquake data comes from the USGS data feed in the JSON format. The Json.Net framework is used to help parse and query the feed with ease. The map module contains an Earthquake repository USGSRepository. The following shows how the list of earthquakes are populated and returned to the viewmodel for binding.

VB
Public Function FindAll() As List(Of [Shared].Earthquake) _
                Implements Infrastructure.IEarthquakeRepository.FindAll
Dim feed As String
Dim feedObject As JObject

Try
Using client As New WebClient
'set header as some site require this to be present
client.Headers.Add("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)")

'download the Json feed
feed = client.DownloadString(New Uri("http://earthquake.usgs.gov/earthquakes/feed/geojson/2.5/month"))

'parse the feed string
feedObject = JObject.Parse(feed)
End Using

Return (From item In feedObject.SelectToken("features")
Select New [Shared].Earthquake(item.SelectToken("id").ToString) With _
       {.Title = item.SelectToken("properties.place").ToString,
.Magnitude = item.SelectToken("properties.mag").ToString,
.Latitude = item.SelectToken("geometry.coordinates[1]"),
.Longitude = item.SelectToken("geometry.coordinates[0]"),
.LinkURL = String.Concat("http://earthquake.usgs.gov", item.SelectToken("properties.url").ToString),
.EventDateTime = UTCDateTimeHelper.UTCTimeConverter(item.SelectToken("properties.time").ToString),
.Source = "USGS"}).ToList
Catch ex As Exception
' consider logging
Return New List(Of [Shared].Earthquake)
End Try
End Function

So lastly the viewmodel. The viewmodel has a basic method that is called to grab this data and populate the observable collection so that the map pins can be positioned at the latitude and longitude co-ordinates.

VB
''' <summary>
''' Sets the earthquake locations.
''' </summary>
''' <remarks></remarks>
Private Sub SetEarthquakeLocations()
_eventAggregator.GetEvent(Of Infrastructure.Events.Composite.StatusUpdatedEvent).Publish("Updating Quake locations...")

task.Factory.StartNew(Sub()
Dim mapModel As New MapModel(New USGSRepository)

For Each quake In mapModel.GetEarthquakeData
If Not EarthquakeLocations.Contains(quake) Then
Application.Current.Dispatcher.BeginInvoke(Sub(q)
'add the new quake item to the earthquakes collection via the dispatcher.
'this is because the observable collection can not be updated from another thread.
EarthquakeLocations.Add(q)
End Sub, {quake})
End If
Next
End Sub).ContinueWith(Sub(antecedent)
Dim message As String
If antecedent.IsFaulted Then
message = "Error getting quake events"
Else
message = "Idle..."
End If

_eventAggregator.GetEvent(Of Infrastructure.Events.Composite.StatusUpdatedEvent).Publish(message)
End Sub)
End Sub

You will notice there is an event aggregator publishing a status message. The status bar subscribes to this event to display the message.

Conclusion

This is just the initial building blocks to a Bing Maps application using WPF. The Bing Maps control is great if you need to plot events or locations on the world map. In the next article I want to provide a means to show real-time events and how this can be used in the application. I hope this article is of some use even though many of the topics such as MEF and MVVM are not discussed in any detail.

History

  • 23/05/2012 - First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect ACPI Investments
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionExcellent article, voted 5 Pin
Paula Scholz17-Oct-13 6:15
Paula Scholz17-Oct-13 6:15 
AnswerRe: Excellent article, voted 5 Pin
db7uk22-Oct-13 20:43
db7uk22-Oct-13 20:43 
GeneralMy Vote of 5 Pin
Dinesh Angappan8-Aug-13 5:10
Dinesh Angappan8-Aug-13 5:10 
GeneralRe: My Vote of 5 Pin
db7uk9-Aug-13 9:54
db7uk9-Aug-13 9:54 
Generalcool app Pin
Rozzie18-Jun-12 11:52
Rozzie18-Jun-12 11:52 
GeneralRe: cool app Pin
db7uk18-Jun-12 21:57
db7uk18-Jun-12 21:57 
QuestionGreat app. Pin
M_m_o_valour18-Jun-12 4:30
M_m_o_valour18-Jun-12 4:30 
AnswerRe: Great app. Pin
db7uk18-Jun-12 8:06
db7uk18-Jun-12 8:06 
GeneralRe: Great app. Pin
M_m_o_valour18-Jun-12 22:39
M_m_o_valour18-Jun-12 22:39 
QuestionCannot download the code Pin
Sanjeev Sharma17-Jun-12 19:33
Sanjeev Sharma17-Jun-12 19:33 
AnswerRe: Cannot download the code Pin
db7uk17-Jun-12 22:13
db7uk17-Jun-12 22:13 
Questionvery nice Pin
BillW337-Jun-12 4:35
professionalBillW337-Jun-12 4:35 
AnswerRe: very nice Pin
db7uk7-Jun-12 4:50
db7uk7-Jun-12 4:50 
GeneralMy vote of 4 Pin
Madhan Mohan Reddy P25-May-12 18:27
professionalMadhan Mohan Reddy P25-May-12 18:27 
GeneralRe: My vote of 4 Pin
db7uk26-May-12 3:00
db7uk26-May-12 3:00 
Question5ed! Pin
El_Codero25-May-12 10:05
El_Codero25-May-12 10:05 
AnswerRe: 5ed! Pin
db7uk26-May-12 2:59
db7uk26-May-12 2:59 
GeneralGreat Job Pin
Tim Corey25-May-12 7:18
professionalTim Corey25-May-12 7:18 
GeneralRe: Great Job Pin
db7uk26-May-12 2:59
db7uk26-May-12 2:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.