Click here to Skip to main content
15,867,835 members
Articles / Programming Languages / C#

Switching to MVVM

Rate me:
Please Sign up or sign in to vote.
4.93/5 (12 votes)
19 May 2011CPOL11 min read 54.2K   1.2K   36   9
How to incorporate MVVM in your existing codebase, step by step, and the risks involved in each step.

1. Introduction

Writing an application with good practices and a proven architecture design is comparatively easy if we are going to write a new application. But most of the time, we are working on existing applications, where change is not easy to make. If we ever get a chance to rewrite an existing application, then probably we won’t repeat the same mistakes which we or someone else did due to reasons such as time constraints, technology limitations, scope creep, etc. It would be better if we can somehow refactor existing codebases for betterment, with no or minimal risk.

MVVM is a proven Design Pattern used heavily in a lot of WPF / Silverlight applications. But it might be possible that we already have lots of code base that is not taking advantage of it. This article is for those who already have an application rather than starting a new one and wants to take an advantage of MVVM. In this article, we are going to see how we can take small steps towards MVVM. There are two major parts to this article. In the first part, we are going to study the evaluation of MVVM. In the second part, we are going to see the different steps to achieve MVVM. Although some steps have major rework and a potential of high risk, there are a few things which have no or minimal effect on the project.

2. Evaluation of MVVM

Let’s take a look at MVVM from a higher level and take a step by step approach to understand it. Our discussion is based on the complexity of the architecture from simple to complex, not from historical order.

Probably the simplest design principle to separate data from its presentation is the Observer design pattern [1]. In The Observer design pattern, we have two different classes for data (subject/model) or its presentation (observer/view). Subject classes contain the instance of all of the observers and send notification to all of the observers when there is any change in the data. Here is a simple block diagram of the Observer design pattern:

Observer Desian Pattern

The next step is to introduce a middle layer in between the data and its presentation. The main purpose of this layer is to communicate between these two components. This is the main concept of Model View Controller (MVC) [2]. It is shown in this block diagram:

MVC

This approach has some advantages and disadvantages. The main disadvantage is that our view is not totally independent of our model. Model View Presenter (MVP) [3] handles exactly the same problem. In the MVP model, there is no relation between the View and the Model.

MVP

MVVM is very similar to the MVP pattern. Or it is some sort of specialized form of MVP pattern. In MVVM, the Presentator is known as the ViewModel. The Model communicates with the ViewModel with notification and the ViewModel communicates with the View with data binding and command binding, as shown by this block diagram:

MVVM

Now let’s take a look at MVVM in a little bit more detail. What is the biggest advantage of this? It's first advantage is that our presentation is totally unaware of our model. We don’t write any user interface specific code in the ViewModel and all the communication is based on data binding and command binding. This means, we can easily write unit tests for it. We can easily change any user interface or even change the data model easily. Here is a detailed block diagram of MVVM:

MVVM

This diagram explains how we can take advantage of MVVM. The most important thing in this pattern is to properly design the ViewModel. WPF has a very orthogonal design. This means we can customize or enhance the different parts of the library without affecting others. Most of WPF reusability is based on composition rather than inheritance. Therefore we can take its maximum advantage even at run time. Because, we can change the composition behavior at run time easily, but not inherited components. In this block diagram, we see the major components of the WPF class library which we develop, enhance, or customize in most WPF applications:

ViewModel

3. Switching to MVVM

We are going to start with a simple application, that is not written in WPF, and gradually make step by step changes in it to introduce the MVVM pattern. Our starting point is a loan amortization application written in VC++ and using WPF [4]. The reason to pick that application is, by definition, we can’t use MVVM with VC++, because XAML support is very limited in VC++ (the only support as of now is to load XAML at runtime and use it).

Although it is possible that we have some properties in our class that stores data, we usually have fields to store information. In our starting project, we have a class to store information about each payment. Here is our class:

C++
public class PaymentInfo
{
    public int PaymentNo
    { get; set; }

    public double Payment
    { get; set; }

    public double Principle
    { get; set; }

    public double Interest
    { get; set; }

    public double Balance
    { get; set; }
}

We have field variables to store information from the user interface and display it back to the user interface.

C++
private double principle;
private double interestRate;
private int duration;
private double payment;

Here is a piece of code to get user input and store it in field variables:

C++
if (txtPrincipleAmount.Text.Length > 0)
{
    principle = Convert.ToDouble(txtPrincipleAmount.Text);
}
else
{
    MessageBox.Show("Please enter principle amount", 
       "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

if (txtInterestRate.Text.Length > 0)
{
    interestRate = Convert.ToDouble(txtInterestRate.Text);
    interestRate /= 100;
    interestRate /= 12;
}
else
{
    MessageBox.Show("Please enter interest", 
      "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

if (txtDuration.Text.Length > 0)
{
    duration = Convert.ToInt32(txtDuration.Text);
}
else
{
    MessageBox.Show("Please enter duration", 
      "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

We have some utility methods to perform our calculations, which we call from our event handler:

C++
// Calculate the remaining balance at particular payment
private double CalculateBalance(int month)
{
    double interestTerm = Math.Pow((1 + interestRate), month);
    double totalInterest = principle * interestTerm;
    double totalPaid = payment * (interestTerm - 1) / interestRate;
    return totalInterest - totalPaid;
}

// Calculate the Interest part of any particular payment
private double CalculateInterestPart(int month)
{
    double interestTerm = Math.Pow((1 + interestRate), (month - 1));
    double totalInterest = principle * interestTerm;
    double totalPaid = payment * (interestTerm - 1) / interestRate;
    return (totalInterest - totalPaid) * interestRate;
}

// Calculate the principle part of any particular payment
private double CalculatePrinciple(int month)
{
    return payment - CalculateInterestPart(month);
}

We have one method to calculate the amortization schedule for the complete loan period and add those values into a DataGrid.

C++
// Calculate the complete amortization schedule and fill the data grid control
private void CalculatePayment()
{
    int totalpayments = duration * 12;

    Title = "Amortization Schedule for " + 
        Convert.ToString(totalpayments) + " Payments";

    // calculate interest term
    double interestTerm = Math.Pow((1 + interestRate), totalpayments);

    // calculate payment
    payment = (principle * interestRate) / (1 - (1 / interestTerm));

    payments.Clear();

    for (int iIndex = 1; iIndex <= totalpayments; ++iIndex)
    {
        PaymentInfo paymentInfo = new PaymentInfo();
        paymentInfo.PaymentNo = iIndex;
        paymentInfo.Balance = CalculateBalance(iIndex);
        paymentInfo.Payment = payment;
        paymentInfo.Interest = CalculateInterestPart(iIndex);
        paymentInfo.Principle = CalculatePrinciple(iIndex);

        payments.Add(paymentInfo);
    }

    lstAmortization.ItemsSource = payments;
}

Note that the user interface code is heavily coupled inside the business logic, in this case calculating the loan amortization. There is no simple way to write test cases to check this calculation without involving the user interface.

The XAML of our project is very simple. Here is the complete XAML code of our program:

XML
<Window x:Class="MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="400" Width="600">
    <Grid Background="Beige">
        <Grid.RowDefinitions>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Column="0" Grid.ColumnSpan="2" 
              Grid.Row="0" FontSize="18" FontWeight="Bold"
              HorizontalAlignment="Center" 
              VerticalAlignment="Center">Loan Amortization</TextBlock>
            <TextBlock Grid.Column="0" Grid.Row="1" 
              VerticalAlignment="Center" Margin="5">Principle Amount</TextBlock>
            <TextBlock Grid.Column="0" Grid.Row="2" 
              VerticalAlignment="Center" Margin="5">Interest Rate</TextBlock>
            <TextBlock Grid.Column="0" Grid.Row="3" 
              VerticalAlignment="Center" Margin="5">Duration</TextBlock>
            <TextBox Grid.Column="1" Grid.Row="1" 
              Margin="5" VerticalAlignment="Center" Name="txtPrincipleAmount"/>
            <TextBox Grid.Column="1" Grid.Row="2" 
              Margin="5" VerticalAlignment="Center" Name="txtInterestRate"/>
            <TextBox Grid.Column="1" Grid.Row="3" 
              Margin="5" VerticalAlignment="Center" Name="txtDuration"/>
        </Grid>
        <DataGrid Grid.Row="1" Name="lstAmortization" Margin="5">
        </DataGrid>       
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button Grid.Column="0" Name="btnCalculate" 
              Width="75" Height="45" 
              Click="btnCalculate_Click">Calculate</Button>
            <Button Grid.Column="1" Name="btnExit" 
              Width="75" Height="45" 
              Click="btnExit_Click">Exit</Button>
        </Grid>
    </Grid>
</Window>

Also note that like other programming styles, here we have a name for every control.

3.1. Step 1: Use Properties

If the code is using fields not properties, then convert those from fields to properties (this should have no or minimal effect on existing source code, because I choose the property name very similar (or same) as fields). We simply change to properties instead of fields in this step. Here is an updated version of our program:

C++
public double Principle
{ get; set; }

public double InterestRate
{ get; set; }

public int Duration
{ get; set; }

public double Payment
{ get; set; }

There is no change to the XAML of the program. If we use the same property name as the previously used field, then we don’t have to change anything in the code. In our example, I use the Letter case for properties and Camel case for fields, so we update the source code accordingly and use Letter case there.

3.2. Step 2: Implement the INotifyPropertyChanged Interface / Dependency Property

My first step is to change the data model to make it WPF friendly. To do this, I will either implement the INotifyPropertyChange interface or make them dependency properties. If we already have properties, then this should have no effect at all for the rest of the project. The rest of the code should be the same and should work the same, but at least now you have your model ready.

In this example, we are going to change our properties to dependency properties. In the following steps, we will see an example of the INotifyPropertyChanged interface too to see both variants. Here is our updated code:

C++
public static readonly DependencyProperty PrincipleProperty =
       DependencyProperty.Register("Principle", typeof(double), typeof(MainWindow));

public double Principle
{
    get { return (double)GetValue(PrincipleProperty); }
    set { SetValue(PrincipleProperty, value); }
}

public static readonly DependencyProperty InterestRateProperty =
    DependencyProperty.Register("InterestRate", typeof(double), typeof(MainWindow));

public double InterestRate
{
    get { return (double)GetValue(InterestRateProperty); }
    set { SetValue(InterestRateProperty, value); }
}

public static readonly DependencyProperty DurationProperty =
    DependencyProperty.Register("Duration", typeof(int), typeof(MainWindow));

public int Duration
{
    get { return (int)GetValue(DurationProperty); }
    set { SetValue(DurationProperty, value); }
}

public static readonly DependencyProperty PaymentProperty =
    DependencyProperty.Register("Payment", typeof(double), typeof(MainWindow));

public double Payment
{
    get { return (double)GetValue(PaymentProperty); }
    set { SetValue(PaymentProperty, value); }
}

There is no change in the XAML file.

3.3. Step 3: Use Data Binding

The next step is to use data binding. This will reduce a lot of codebase. Now it shouldn't have any code like:

C++
txtName.Text = firstName;

This is a major change in the code base. But in this step, I focus only on the data not on the behavior. My code still has an event handler and should work fine, but now I don’t have to worry about updating the data into the control or getting data from the control to my variable. (No more control to variable interaction.) Now we only check the invalid input directly from the properties (they are automatically getting values from the user interface). Here is our validation code now:

C++
if (Principle < 0)
{
    MessageBox.Show("Please enter principle amount", 
       "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

if (InterestRate < 0)
{
    MessageBox.Show("Please enter interest", "Error", 
      MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

if (Duration < 0)
{
    MessageBox.Show("Please enter duration", 
      "Error", MessageBoxButton.OK, MessageBoxImage.Error);
    return;
}

Now we have to update our XAML and use the data binding there. Here is the updated version of our XAML:

XML
<TextBox Grid.Column="1" Grid.Row="1" 
  Margin="5" VerticalAlignment="Center" Text="{Binding Principle}" />
<TextBox Grid.Column="1" Grid.Row="2" 
  Margin="5" VerticalAlignment="Center" Text="{Binding InterestRate}"/>
<TextBox Grid.Column="1" Grid.Row="3" 
  Margin="5" VerticalAlignment="Center" Text="{Binding Duration}"/>

Also note that now we no longer need to specify the name of the control; with the help of data binding, we will automatically get the values in the correct property (dependency property in this example).

3.4. Step 4: Refactor Event Handler

This is an intermediate step. I already have all the data handling in the form of data binding with no control/variable code in it. I am going to iterate through all the event handlers and whatever code is written there, move them into a method and call that method from the event handler. This is the updated version of our event handler now:

C++
private void btnCalculate_Click(object sender, RoutedEventArgs e)
{
    CalculateAmortization();
}

And the CalculateAmortization method will do the rest of the work. Here is the code for the CalculateAmortization method:

C++
private void CalculateAmortization()
{
    ValidateInput();
    CalculatePayment();
}

We refactor our code into two different methods: one to do the validation and the other to perform the actual calculation.

3.5. Step 5: Implement the ICommand Interface

In the next step, I am going to introduce the ICommand interface and implement it. There is no change for the existing codebase. Here is an example:

C++
public class MyCommand : ICommand
{
    public Action Function
    { get; set; }

    public MyCommand()
    {
    }

    public MyCommand(Action function)
    {
        Function = function;
    }

    public bool CanExecute(object parameter)
    {
        if (Function != null)
        {
            return true;
        }

        return false;
    }

    public void Execute(object parameter)
    {
        if (Function != null)
        {
            Function();
        }
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
}

There is no change in the XAML of our project.

3.6. Step 6: Add the ICommand Properties

Now I am going to add properties in my class (which had the event handler earlier). The type of the property is ICommand. This again should not have any effect on the rest of the code. The number of properties should be at least the same as the method I called inside the event handler and I choose a similar name.

C++
public ICommand ExitCommand
{ get; set; }

public ICommand CalculateAmortizationCommand
{ get; set; }

There is no change in the XAML of our project.

3.7. Step 7: Assign Methods to the ICommand Type Properties

I have already created a method to calculate amortization (or do other work). Create an object of the MyCommand class and assign it to our ICommand properties in the constructor.

C++
ExitCommand = new MyCommand(Close);
CalculateAmortizationCommand = new MyCommand(CalculateAmortization);

There is no change in the XAML of our project.

3.8. Step 8: Use Command Binding

Remove the event handler from the code as well as from the XAML and instead of it, use command binding. This is again a major change in the code (both code and XAML).

XML
<Button Grid.Column="0" Name="btnCalculate" Width="75" 
  Height="45" Command="{Binding CalculateAmortizationCommand}" >Calculate</Button>
<Button Grid.Column="1" Name="btnExit" Width="75" 
  Height="45" Command="{Binding ExitCommand}" >Exit</Button>

Also note that now all of our business logic is in the method, not in the event handler. This means now we can somehow perform unit testing on those methods without any user interface involvement.

3.9. Step 9: Define the ViewModel Class

Define a class to the ViewModel and move all the ICommand interface properties and methods assigned to it in this class. We introduce an event type property in our ViewModel class to handle the close event.

C++
public event EventHandler RequestClose;

private void CloseWindow()
{
    EventHandler handler = this.RequestClose;

    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

We set this event handler from our window (view) class. There are multiple ways to do this, but here we are doing this with an unnamed delegate.

C++
public partial class MainWindow : Window
{
    private MyViewModel vm = new MyViewModel();

    public MainWindow()
    {
        InitializeComponent();

        vm.RequestClose += delegate
        {
            Close();
        };

        DataContext = vm;
    }
}

Here is the complete code of our ViewModel class:

C++
public class MyViewModel : DependencyObject
{
    public static DependencyProperty PrincipleProperty =
        DependencyProperty.Register("Principle", typeof(double), typeof(MainWindow));

    public double Principle
    {
        get { return (double)GetValue(PrincipleProperty); }
        set { SetValue(PrincipleProperty, value); }
    }

    public static DependencyProperty InterestRateProperty =
        DependencyProperty.Register("InterestRate", typeof(double), typeof(MainWindow));

    public double InterestRate
    {
        get { return (double)GetValue(InterestRateProperty); }
        set { SetValue(InterestRateProperty, value); }
    }

    public static DependencyProperty DurationProperty =
        DependencyProperty.Register("Duration", typeof(int), typeof(MainWindow));

    public int Duration
    {
        get { return (int)GetValue(DurationProperty); }
        set { SetValue(DurationProperty, value); }
    }

    public static DependencyProperty PaymentProperty =
        DependencyProperty.Register("Payment", typeof(double), typeof(MainWindow));

    public double Payment
    {
        get { return (double)GetValue(PaymentProperty); }
        set { SetValue(PaymentProperty, value); }
    }

    public event EventHandler RequestClose;

    public ObservableCollection<PaymentInfo> Payments
    { get; set; }

    public ICommand CalculateAmortizationCommand
    { get; set; }

    public ICommand ExitCommand
    { get; set; }

    public MyViewModel()
    {            
        CalculateAmortizationCommand = new MyCommand(CalculateAmortization);
        ExitCommand = new MyCommand(CloseWindow);

        Payments = new ObservableCollection<PaymentInfo>();
    }

    private void CloseWindow()
    {
        EventHandler handler = this.RequestClose;

        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void CalculateAmortization()
    {
        ValidateInput();
        CalculatePayment();
    }

    // Validate Input
    private void ValidateInput()
    {
        if (Principle < 0)
        {
            MessageBox.Show("Please enter principle amount", 
              "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }

        if (InterestRate < 0)
        {
            MessageBox.Show("Please enter interest", 
              "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }

        if (Duration < 0)
        {
            MessageBox.Show("Please enter duration", 
              "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            return;
        }
    }

    // Calculate the complete amortization schedule and fill the list control
    private void CalculatePayment()
    {
        int totalpayments = Duration * 12;

        double monthlyInterest = CalculateMonthlyInterest(InterestRate);

        // calculate interest term
        double interestTerm = Math.Pow((1 + monthlyInterest), totalpayments);

        // calculate payment
        Payment = (Principle * monthlyInterest) / (1 - (1 / interestTerm));

        Payments.Clear();

        for (int iIndex = 1; iIndex <= totalpayments; ++iIndex)
        {
            PaymentInfo paymentInfo = new PaymentInfo();
            paymentInfo.PaymentNo = iIndex;
            paymentInfo.Balance = CalculateBalance(iIndex);
            paymentInfo.Payment = Payment;
            paymentInfo.Interest = CalculateInterestPart(iIndex);
            paymentInfo.Principle = CalculatePrinciple(iIndex);

            Payments.Add(paymentInfo);
        }

        //lstAmortization.ItemsSource = payments;
    }

    // Calculate the remaining balance at particular payment
    private double CalculateBalance(int month)
    {
        double monthlyInterest = CalculateMonthlyInterest(InterestRate);

        double interestTerm = Math.Pow((1 + monthlyInterest), month);
        double totalInterest = Principle * interestTerm;
        double totalPaid = Payment * (interestTerm - 1) / monthlyInterest;
        return totalInterest - totalPaid;
    }

    // Calculate the Interest part of any particular payment
    private double CalculateInterestPart(int month)
    {
        double monthlyInterest = CalculateMonthlyInterest(InterestRate);

        double interestTerm = Math.Pow((1 + monthlyInterest), (month - 1));
        double totalInterest = Principle * interestTerm;
        double totalPaid = Payment * (interestTerm - 1) / monthlyInterest;
        return (totalInterest - totalPaid) * monthlyInterest;
    }

    // Calculate the principle part of any particular payment
    private double CalculatePrinciple(int month)
    {
        return Payment - CalculateInterestPart(month);
    }

    // Calculate the monthly interest rate
    private double CalculateMonthlyInterest(double InterestRate)
    {
        double monthlyInterest = InterestRate;
        monthlyInterest /= 100;
        monthlyInterest /= 12;

        return monthlyInterest;
    }
}

Note that this class doesn’t have any user interface element (other than a few message boxes in data validation, which can be removed easily if use the DataValidation in XAML), so we can easily write test cases on it. In addition to this, if we want to change the presentation of this, such as WPF to Silverlight or Console based application, we can do it very easily. This class contains all the business logic and now it is up to the user of this class how to present that information.

3.10. Step 10: Define the ViewModelBase Class

Let’s do one more step. We might want to do the same thing again and again, so why not create a base class to have a minimum ViewModel functionality to reuse it in other ViewModel classes. One more reason to create a ViewModelBase class is to show how to implement the INotifyPropertyChanged interface. Here is the code of our base class:

C++
public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event EventHandler RequestClose;

    public ICommand ExitCommand
    { get; set; }

    public void Close()
    {
        EventHandler handler = this.RequestClose;

        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }        
    }

    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Also note that we made our ViewModelBase class abstract to avoid creating an object of this class. Here is part of our ViewModel class inherited by ViewModelBase.

C++
public class MyViewModel : ViewModelBase
{
    public double principle;
    public double interestRate;
    public int duration;
    public double payment;

    public double Principle
    {
        get { return principle; }
        set
        {
            principle = value;
            RaisePropertyChanged("Principle");
        }
    }

    public double InterestRate
    {
        get { return interestRate; }
        set
        {
            interestRate = value;
            RaisePropertyChanged("InterestRate");
        }
    }

    public int Duration
    {
        get { return duration; }
        set
        {
            duration = value;
            RaisePropertyChanged("Duration");
        }
    }

    public double Payment
    {
        get { return payment; }
        set
        {
            payment = value;
            RaisePropertyChanged("Payment");
        }
    }
}

In this class, we raise the property change event whenever there is a change in the property. There are lots of other things we can do to improve our code base, but this is just a first step in the right direction. We can easily include more steps in the following guidelines, but after following these steps, we will have a reasonably good design of our program towards MVVM.

Here is the output of this program.

Loan Amortization Output

4. References

  1. Design Patterns, Elements of Reusable Object Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.
  2. A Cookbook for Using the Model-View-Controller User Interface Paradigm in Smalltalk-80, Glenn E. Krasner, Stephen T. Pope, Journal of Object Oriented Programming, August/September 1988 http://www.ics.uci.edu/~redmiles/ics227-SQ04/papers/KrasnerPope88.pdf.
  3. GUI Architectures, Martin Fowler, http://www.martinfowler.com/eaaDev/uiArchs.html.
  4. Loan Amortization Application in WPF using C++, Zeeshan Amjad, LoanAmortizationWPF.aspx, http://www.codeguru.com/cpp/cpp/cpp_managed/general/print.php/c16355/.

License

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


Written By
Team Leader American Institute for Research
United States United States
Working as a Team leader in American Institute for Research

Comments and Discussions

 
GeneralSection and Tags Pin
Jani Giannoudis19-May-11 20:36
Jani Giannoudis19-May-11 20:36 
GeneralRe: Section and Tags Pin
Zeeshan Amjad20-May-11 1:53
Zeeshan Amjad20-May-11 1:53 

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.