Click here to Skip to main content
16,020,381 members
Articles / Desktop Programming / XAML

A Sample Silverlight 4 Application Using MEF, MVVM, and WCF RIA Services - Part 1

Rate me:
Please Sign up or sign in to vote.
4.84/5 (108 votes)
7 Jul 2011CPOL9 min read 2.1M   30.9K   298   278
Part 1 of a series describing the creation of a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.

Article Series

This article is part one of a series on developing a Silverlight business application using MEF, MVVM Light, and WCF RIA Services.

Image 1

Contents

Introduction

This sample application is the result of my initiative to learn Silverlight and WCF RIA Services. With my background of using WPF and MVVM for the past several years, I found that there is a lack of sample LOB applications that can combine the latest Silverlight enhancements with MVVM. This three part article series is my effort at creating such a sample. The choice of an issue tracking application comes from David Poll's PDC09 talk, and the design architecture is from Shawn Wildermuth's blog posts.

The main features of this issue tracking application are:

  • Login screen provides custom authentication and password reset based on security question and answer.
  • My Profile screen is for updating user information, password, security questions and answers.
  • User Maintenance screen is only available to Admin users, and lets the Admin user add/delete/update users.
  • New Issue screen is for creating new issues (bugs, work items, spec defects, etc.).
  • My Issues screen is for tracking all active and resolved issues assigned to a user.
  • All Issues screen is for tracking all issues (Open, Active, Pending, or Resolved).
  • Bug Report screen provides a summary of bug trend, bug count, and the functionality to print the summary.
  • Four different Themes are available and can be applied dynamically at any time.

Requirements

In order to build the sample application, you need:

  • Microsoft Visual Studio 2010 SP1
  • Silverlight 4 Toolkit April 2010 (included in the sample solution)
  • MVVM Light Toolkit V3 SP1 (included in the sample solution)

Installation

After downloading the setup package to a location on your local disk, we need to complete the following steps:

1. Installing the IssueVision Sample Database

To install the sample database, please run SqlServer_IssueVision_Schema.sql and SqlServer_IssueVision_InitialDataLoad.sql included in the setup package zip file. SqlServer_IssueVision_Schema.sql creates the database schema and database user IVUser; SqlServer_IssueVision_InitialDataLoad.sql loads all the data needed to run this application, including the initial application user ID user1 and Admin user ID admin1, with passwords all set as P@ssword1234.

Image 2

2. Installing the Web Setup Package

After the database setup, run setup.exe also included in the setup package zip file. This will install the IssueVision for Silverlight website.

Image 3

When done installing the website, we can access the Silverlight application as follows:

Image 4

Architecture

1. Solution Structure

Inside the sample solution file, projects are further organized into either the Client folder or the Server folder. The Client folder includes all the projects that will be compiled into the file IssueVision.Client.xap, and the Server folder consists of all the projects that will eventually run inside a web server environment.

Image 5

For the projects inside the Server folder:

  • IssueVision.Web project is the main startup project. It includes the startup page Default.aspx and the Silverlight application package IssueVision.Client.xap.
  • IssueVision.Data.Web project is the server-side data access layer. It receives requests from clients, accesses the database through the database user IVUser, and returns the results back. The major components of this project include the IssueVision Entity data model and all related DomainService classes.

For the projects inside the Client folder:

  • IssueVision.Data project has a WCF RIA Services link to IssueVision.Data.Web, and therefore hosts the generated client-side proxy code and shared source code. This project also includes all the client-side only partial classes that do not need to be duplicated on the server side.
  • IssueVision.Common project, as the name suggests, includes all the common interface classes and helper classes shared among other client projects.
  • IssueVision.Model project defines the Model of MVVM, and it has the following three model classes:
    • AuthenticationModel
    • PasswordResetModel
    • IssueVisionModel
  • IssueVision.ViewModel project is the ViewModel part of MVVM, and includes all the nine ViewModel classes.
  • IssueVison.Client is the main client-side project, and is also the View of MVVM that hosts all the UI logic.

From the solution structure above, we should notice that MVVM provides good separation of concerns between the UI and the business logic in order to make those UIs easier to maintain by developers and designers. Next, let's visit the Model, ViewModel, and View classes in more detail.

2. IssueVisionModel Class

We will discuss the classes AuthenticationModel and PasswordResetModel in part 3. For now, let's focus on the class IssueVisionModel, the main Model (of MVVM) class for this application. IssueVisionModel is based on the following interface, IIssueVisionModel:

C#
public interface IIssueVisionModel : INotifyPropertyChanged
{
    void GetIssueTypesAsync();
    event EventHandler<EntityResultsArgs<IssueType>> GetIssueTypesComplete;
    void GetPlatformsAsync();
    event EventHandler<EntityResultsArgs<Platform>> GetPlatformsComplete;
    void GetResolutionsAsync();
    event EventHandler<EntityResultsArgs<Resolution>> GetResolutionsComplete;
    void GetStatusesAsync();
    event EventHandler<EntityResultsArgs<Status>> GetStatusesComplete;
    void GetSubStatusesAsync();
    event EventHandler<EntityResultsArgs<SubStatus>> GetSubStatusesComplete;
    void GetUsersAsync();
    event EventHandler<EntityResultsArgs<User>> GetUsersComplete;
    void GetCurrentUserAsync();
    event EventHandler<EntityResultsArgs<User>> GetCurrentUserComplete;
    void GetSecurityQuestionsAsync();
    event EventHandler<EntityResultsArgs<SecurityQuestion>> 
          GetSecurityQuestionsComplete;
    void GetMyIssuesAsync();
    event EventHandler<EntityResultsArgs<Issue>> GetMyIssuesComplete;
    void GetAllIssuesAsync();
    event EventHandler<EntityResultsArgs<Issue>> GetAllIssuesComplete;
    void GetAllUnresolvedIssuesAsync();
    event EventHandler<EntityResultsArgs<Issue>> GetAllUnresolvedIssuesComplete;

    void GetActiveBugCountByMonthAsync(Int32 numberOfMonth);
    event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByMonthComplete;
    void GetResolvedBugCountByMonthAsync(Int32 numberOfMonth);
    event EventHandler<InvokeOperationEventArgs> GetResolvedBugCountByMonthComplete;
    void GetActiveBugCountByPriorityAsync();
    event EventHandler<InvokeOperationEventArgs> GetActiveBugCountByPriorityComplete;

    Issue AddNewIssue();

    void RemoveAttribute(IssueVision.Data.Web.Attribute attribute);
    void RemoveFile(IssueVision.Data.Web.File file);

    User AddNewUser();
    void RemoveUser(IssueVision.Data.Web.User user);

    void SaveChangesAsync();
    event EventHandler<SubmitOperationEventArgs> SaveChangesComplete;
    void RejectChanges();

    Boolean HasChanges { get; }
    Boolean IsBusy { get; }
}

We define a separate Model class and do not use the data context class itself as the Model because the Model is best expressed as a set of properties and operations that retrieve, add, delete, and update data. This makes the Model easier to maintain and test. Additionally, as Shawn mentioned in his blog, "creating a custom Model allows us to isolate what transport layer we're using so we can change it or even have several data providers specifying data for our Model".

Next, let's look at how a retrieve method in the IssueVisionModel class is actually implemented:

C#
public void GetIssueTypesAsync()
{
    PerformQuery(Context.GetIssueTypesQuery(), GetIssueTypesComplete);
}

GetIssueTypeAsync() calls the private method PerformQuery() and passes in an EntityQuery GetIssueTypesQuery() and an event GetIssueTypesComplete. When the retrieve call is done, the event GetIssueTypesComplete will fire and pass back the result set, or any error message if something goes wrong. In fact, almost all retrieve methods are as simple as calling the PerformQuery() method defined below:

C#
private void PerformQuery<T>(EntityQuery<T> qry, 
        EventHandler<EntityResultsArgs<T>> evt) where T : Entity
{
    Context.Load(qry, LoadBehavior.RefreshCurrent, r =>
    {
        if (evt != null)
        {
            try
            {
                if (r.HasError)
                {
                    evt(this, new EntityResultsArgs<T>(r.Error));
                    r.MarkErrorAsHandled();
                }
                else
                {
                    evt(this, new EntityResultsArgs<T>(r.Entities));
                }
            }
            catch (Exception ex)
            {
                evt(this, new EntityResultsArgs<T>(ex));
            }
        }
    }, null);
}

Also, the Model class exports itself to the ViewModel classes by using the MEF Export attribute on the class as follows:

C#
[Export(typeof(IIssueVisionModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel

3. The ViewModel Classes

Most of the ViewModel classes include six regions: Private Data Members region, Constructor region, Public Properties region, Public Commands region, ICleanup Interface region, and Private Methods region. The Public Properties and Public Commands regions expose all the necessary properties and commands to its View class. And, the constructor sets up event handling, sets the initial values for any private data, and registers the AppMessages needed inside the ViewModel class. Here is an example:

C#
[ImportingConstructor]
public IssueEditorViewModel(IIssueVisionModel issueVisionModel)
{
    _issueVisionModel = issueVisionModel;

    // set up event handling
    _issueVisionModel.GetIssueTypesComplete += _issueVisionModel_GetIssueTypesComplete;
    _issueVisionModel.GetPlatformsComplete += _issueVisionModel_GetPlatformsComplete;
    _issueVisionModel.GetResolutionsComplete += _issueVisionModel_GetResolutionsComplete;
    _issueVisionModel.GetStatusesComplete += _issueVisionModel_GetStatusesComplete;
    _issueVisionModel.GetSubStatusesComplete += _issueVisionModel_GetSubStatusesComplete;
    _issueVisionModel.GetUsersComplete += _issueVisionModel_GetUsersComplete;

    // set _currentIssueCache to null
    _currentIssueCache = null;

    // load issue type entries
    IssueTypeEntries = null;
    _issueVisionModel.GetIssueTypesAsync();
    // load platform entries
    PlatformEntries = null;
    _issueVisionModel.GetPlatformsAsync();
    //load resolution entries
    ResolutionEntriesWithNull = null;
    _issueVisionModel.GetResolutionsAsync();
    // load status entries
    StatusEntries = null;
    _issueVisionModel.GetStatusesAsync();
    // load substatus entries
    SubstatusEntriesWithNull = null;
    _issueVisionModel.GetSubStatusesAsync();
    // load user entries
    UserEntries = null;
    UserEntriesWithNull = null;
    _issueVisionModel.GetUsersAsync();

    // register for EditIssueMessage
    AppMessages.EditIssueMessage.Register(this, OnEditIssueMessage);
}

We can see from the code above that the ViewModel class gets an object that implements the IIssueVisionModel interface by using the ImportingConstructor attribute which tells MEF to supply the discovered model class into the ViewModel. In turn, all the ViewModel classes export themselves like the following:

C#
[Export(ViewModelTypes.IssueEditorViewModel, typeof(ViewModelBase))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class IssueEditorViewModel : ViewModelBase

4. The View Classes and the Code-behind Files

Before we discuss any View class, let us first take a look at how a global CompositionContainer object is defined inside the file App.xaml.cs.

C#
public partial class App : Application
{
    // CompositionContainer for the whole application
    public static CompositionContainer Container;

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

        InitializeComponent();
    }

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        Container = new CompositionContainer(new DeploymentCatalog());
        CompositionHost.Initialize(Container);
        CompositionInitializer.SatisfyImports(this);
        RootVisual = new MainPage();
    }
    ......
}

With access to the static Container object, we can easily request a new ViewModel object as follows:

C#
// Use MEF To load the View Model
_viewModelExport = App.Container.GetExport<ViewModelBase>(
                       ViewModelTypes.AllIssuesViewModel);
if (_viewModelExport != null) DataContext = _viewModelExport.Value;

And, we can release a ViewModel object with the following three lines of code:

C#
// set DataContext to null and call ReleaseExport()
DataContext = null;
App.Container.ReleaseExport(_viewModelExport);
_viewModelExport = null;

Each View class finds its ViewModel object through a function call to _viewModelExport = App.Container.GetExport(), followed by DataContext = _viewModelExport.Value in the constructor of each View class. This function instructs MEF at runtime to fulfill a chain of dependencies, which in turn creates all the Model and ViewModel objects required. The beauty of using MEF is that we can keep these projects loosely coupled. In fact, the projects IssueVision.Model, IssueVision.ViewModel, and IssueVison.Client do not need a reference to the other two projects to compile successfully. The project IssueVison.Client has a reference to the other two projects only because we need to add them into the output IssueVision.Client.xap file.

In the same constructor, we also register AppMessages the View class will handle. The IssueEditor class below is a good sample:

C#
public partial class IssueEditor : UserControl, ICleanup
{
    #region "Private Data Members"
    private Lazy<ViewModelBase> _viewModelExport;
    #endregion "Private Data Members"

    #region "Constructor"
    public IssueEditor()
    {
        InitializeComponent();

        // register for ReadOnlyIssueMessage
        AppMessages.ReadOnlyIssueMessage.Register(this, OnReadOnlyIssueMessage);
        // register for OpenFileMessage
        AppMessages.OpenFileMessage.Register(this, OnOpenFileMessage);
        // register for SaveFileMessage
        AppMessages.SaveFileMessage.Register(this, OnSaveFileMessage);

        if (!ViewModelBase.IsInDesignModeStatic)
        {
            // Use MEF To load the View Model
            _viewModelExport = App.Container.GetExport<ViewModelBase>(
                ViewModelTypes.IssueEditorViewModel);
            if (_viewModelExport != null) DataContext = _viewModelExport.Value;
        }
    }
    #endregion "Constructor"

    .........
}

Within the code-behind files, we define all the UI-related logic like event handlers to dynamically enable/disable a button or AppMessages that display an error message when something goes wrong. As long as the code is related to the UI logic, it is perfectly OK to add them into the code-behind file, like the following:

C#
private void userNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    // dynamically enable/disable error message
    if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
        loginScreenErrorMessageTextBox.Text = string.Empty;

    // dynamically enable/disable login button
    loginButton.IsEnabled = 
      !(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
        string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}

private void passwordPasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
    // dynamically enable/disable error message
    if (!string.IsNullOrWhiteSpace(loginScreenErrorMessageTextBox.Text))
        loginScreenErrorMessageTextBox.Text = string.Empty;

    // dynamically enable/disable login button
    loginButton.IsEnabled = 
      !(string.IsNullOrWhiteSpace(userNameTextBox.Text) ||
        string.IsNullOrWhiteSpace(passwordPasswordBox.Password));
}

Custom Controls for Layout

The custom controls FlipControl and MainPageControl defined in the project IssueVision.Common are used as the basis for screen layout.

FlipControl is used in LoginForm.xaml, which hosts both the login screen and the password reset screen. Switching the Dependency Property IsFlipped will toggle between these two screens, with animations defined inside the VisualStateManager.

Similarly, MainPageControl is used in MainPage.xaml, and divides the whole screen into title content, login/logout menu contents, login page content, and main page content. The Dependency Property IsLoggedIn switches between the login page and the main page.

Image 6

Using this layout, custom controls could be considered as another application of separation of concerns. The screen layout styles along with animations defined in VisualStateManager is encapsulated by itself. As long as they provide the same functionality, we can easily change them, let's say creating a new animation, without affecting any View classes defined in the project IssueVision.Client.

Dynamic Theming

There are four different themes defined in this application, and they are BureauBlue, ExpressionLight, ShinyBlue, and TwilightBlue. Each theme is included in the project IssueVision.Client as a ResourceDictionary, which defines all the styles for built-in controls as well as styles for custom controls built specifically for this sample. They are in the Assets folder shown below:

Image 7

When we want to dynamically change themes, ChangeThemmeCommand will get called, and following is the source code:

C#
private RelayCommand<string> _changeThemeCommand = null;
  
public RelayCommand<string> ChangeThemeCommand
{
    get
    {
        if (_changeThemeCommand == null)
        {
            _changeThemeCommand = new RelayCommand<string>(
                OnChangeThemeCommand,
                g =>
                    {
                        var themeResource = Application.GetResourceStream
                            (new Uri("/IssueVision.Client;component/Assets/" + 
                            g, UriKind.Relative));
                        return themeResource != null;
                    });
        }
        return _changeThemeCommand;
    }
}

private void OnChangeThemeCommand(String g)
{
    try
    {
        if (g == "BureauBlue.xaml" || g == "ExpressionLight.xaml" ||
            g == "ShinyBlue.xaml" || g == "TwilightBlue.xaml")
        {
            // remove the old one
            Application.Current.Resources.MergedDictionaries.RemoveAt
                (Application.Current.Resources.MergedDictionaries.Count - 1);
            // find and add the new one
            var themeResource = Application.GetResourceStream(new Uri
                ("/IssueVision.Client;component/Assets/" + 
                g, UriKind.Relative));
            var rd = (ResourceDictionary)(XamlReader.Load
                (new StreamReader(themeResource.Stream).ReadToEnd()));
            Application.Current.Resources.MergedDictionaries.Add(rd);

            // notify the change
            if (g == "BureauBlue.xaml")
            {
                IsBureauBlueTheme = true;
                IsExpressionLightTheme = false;
                IsShinyBlueTheme = false;
                IsTwilightBlueTheme = false;
            }
            else if (g == "ExpressionLight.xaml")
            {
                IsBureauBlueTheme = false;
                IsExpressionLightTheme = true;
                IsShinyBlueTheme = false;
                IsTwilightBlueTheme = false;
            }
            else if (g == "ShinyBlue.xaml")
            {
                IsBureauBlueTheme = false;
                IsExpressionLightTheme = false;
                IsShinyBlueTheme = true;
                IsTwilightBlueTheme = false;
            }
            else if (g == "TwilightBlue.xaml")
            {
                IsBureauBlueTheme = false;
                IsExpressionLightTheme = false;
                IsShinyBlueTheme = false;
                IsTwilightBlueTheme = true;
            }
        }
    }
    catch (Exception ex)
    {
        // notify user if there is any error
        AppMessages.RaiseErrorMessage.Send(ex);
    }
}

I like the flexibility of using ResourceDictionary directly for dynamic theming because we can easily modify them any time there is a bug found or any enhancements are needed. Also, we have the option to define our own styles for any custom controls, as follows:

XML
<ResourceDictionary>

    .........

    <!--IssueVision Specific Styles-->

    <LinearGradientBrush x:Key="IssueVisionBackgroundBrush" EndPoint="1,0.5" 
    StartPoint="0,0.5">
        <GradientStop Color="#FFBFDBFF" Offset="0"/>
        <GradientStop Color="#FFA6C2E5" Offset="1"/>
    </LinearGradientBrush>

    <!--common:MainPageControl-->
    <Style TargetType="common:MainPageControl">
        <Setter Property="Background" 
        Value="{StaticResource IssueVisionBackgroundBrush}"/>
    </Style>

    <!--common:FlipControl-->
    <Style TargetType="common:FlipControl">
        <Setter Property="Background" 
        Value="{StaticResource IssueVisionBackgroundBrush}"/>
        <Setter Property="BorderBrush" Value="DarkBlue"/>
        <Setter Property="BorderThickness" Value="3"/>
        <Setter Property="CornerRadius" Value="4"/>
    </Style>

</ResourceDictionary>

Next Steps

In this article, we visited how the application is installed, as well as the design architecture, layout custom controls, and dynamic theming. In part 2, we will go through the topics of how the MVVM Light Toolkit is used: namely, RelayCommand, Messenger, EventToCommand, and ICleanup.

I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

History

  • May 2010 - Initial release
  • July 2010 - Minor update based on feedback
  • November 2010 - Update to support VS2010 Express Edition
  • February 2011 - Update to fix multiple bugs including memory leak issues
  • March 2011 - Built with Visual Studio 2010 SP1
  • June 2011 - Update based on feedback
  • July 2011 - Update to fix multiple bugs

License

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


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
GeneralRe: regarding installation Pin
phunsuk20-Jul-10 12:30
phunsuk20-Jul-10 12:30 
GeneralRe: regarding installation Pin
Weidong Shen20-Jul-10 14:01
Weidong Shen20-Jul-10 14:01 
GeneralSetup failure Pin
ndtenr28-Jul-10 0:41
ndtenr28-Jul-10 0:41 
GeneralError inserting new Issue Pin
ksafford14-Jul-10 6:31
ksafford14-Jul-10 6:31 
GeneralRe: Error inserting new Issue Pin
Weidong Shen14-Jul-10 7:56
Weidong Shen14-Jul-10 7:56 
GeneralAwesome Example Pin
ksafford13-Jul-10 7:04
ksafford13-Jul-10 7:04 
GeneralRe: Awesome Example Pin
Weidong Shen13-Jul-10 10:24
Weidong Shen13-Jul-10 10:24 
QuestionMetadata attributes are defined in separated class (User.cs) and in 'IssueVisionService.metadata.cs' ? Pin
sheyenrath30-Jun-10 0:31
sheyenrath30-Jun-10 0:31 
Hello,

I see that for some classes (Issue, Attribute, ...) the metadata attributes are just defined in the ''IssueVisionService.metadata.cs' class (probably autogenerated).
But the for the User, the metadata attributes + extra attributes are defined in User.cs

Is there a special reason for this ?

I think it would be clearer to :
- use POCO classes (autogenerated as described on my other post)
- create a separate partial class for every entity which defines metadata + extra attributes

What's your opinion about this ?
AnswerRe: Metadata attributes are defined in separated class (User.cs) and in 'IssueVisionService.metadata.cs' ? Pin
Weidong Shen30-Jun-10 3:21
Weidong Shen30-Jun-10 3:21 
GeneralSome code which explains how to enable WSDL on de RIA Services and connect to it [modified] Pin
sheyenrath30-Jun-10 0:26
sheyenrath30-Jun-10 0:26 
GeneralRe: Some code which explains how to enable WSDL on de RIA Services and connect to it Pin
Weidong Shen30-Jun-10 3:24
Weidong Shen30-Jun-10 3:24 
GeneralRe: Some code which explains how to enable WSDL on de RIA Services and connect to it Pin
sheyenrath30-Jun-10 5:01
sheyenrath30-Jun-10 5:01 
GeneralRe: Some code which explains how to enable WSDL on de RIA Services and connect to it Pin
Weidong Shen30-Jun-10 6:03
Weidong Shen30-Jun-10 6:03 
QuestionConcurrent update problem ? Pin
sheyenrath29-Jun-10 3:24
sheyenrath29-Jun-10 3:24 
AnswerRe: Concurrent update problem ? Pin
Weidong Shen29-Jun-10 4:01
Weidong Shen29-Jun-10 4:01 
GeneralUse POCO's instead of EF generated entities Pin
sheyenrath29-Jun-10 3:23
sheyenrath29-Jun-10 3:23 
GeneralRe: Use POCO's instead of EF generated entities Pin
Weidong Shen29-Jun-10 4:49
Weidong Shen29-Jun-10 4:49 
QuestionHow to use MVVM Light Toolkit with ChildWindows? Pin
ibobli28-Jun-10 20:08
ibobli28-Jun-10 20:08 
AnswerRe: How to use MVVM Light Toolkit with ChildWindows? Pin
Weidong Shen29-Jun-10 6:28
Weidong Shen29-Jun-10 6:28 
GeneralUsing DevForce as the model Pin
Stuffandstuff28-Jun-10 6:13
Stuffandstuff28-Jun-10 6:13 
GeneralRe: Using DevForce as the model Pin
Weidong Shen28-Jun-10 6:51
Weidong Shen28-Jun-10 6:51 
GeneralRe: Using DevForce as the model Pin
Stuffandstuff28-Jun-10 7:02
Stuffandstuff28-Jun-10 7:02 
GeneralMy vote of 5 Pin
Stuffandstuff26-Jun-10 8:30
Stuffandstuff26-Jun-10 8:30 
GeneralRe: My vote of 5 Pin
Weidong Shen26-Jun-10 11:24
Weidong Shen26-Jun-10 11:24 
GeneralError when Submitting Pin
maqinfo25-Jun-10 23:27
maqinfo25-Jun-10 23:27 

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.