Click here to Skip to main content
14,217,648 members

Writing a XAML dialog application for X11

Rate this:
5.00 (6 votes)
Please Sign up or sign in to vote.
5.00 (6 votes)
10 Oct 2014CPOL
Currently none of the big Linux/Unix (X11) GUI application frameworks (GTK+, KDE) support XAML based application development. The Moonlight project (including XAML support) was abandoned on May 29, 2012. This article shows 'how to' a XAML based dialog application with the Roma Widget Set (Xrw) in C#

Introduction

This article is a hands-on tutorial, how to write a MVVM (Model View ViewModel) design pattern based X11 dialog interface (simple GUI) application with XAML using the Roma Widget Set (Xrw). The Roma Widget Set is a zero dependency GUI application framework for X11 (it requires only assemblies of the free Mono standard installation and libraries of the free X11 distribution; it doesn't particularly require GNOME, KDE or commercial libraries) and is implemented entirely in C#.

As far as i know, this (utilizing the Xrw) is the first attempt to use XAML for X11 application development after the abandonment of Moonlight.

Neither the Roma Widget Set nor the XAML implementation are complete. This sample application is intended as a 'proof of concept' and checks out if and how it is possible bo create MVVM design pattern based X11 application with XAML.

The next article about XAML using the Roma Widget Set on X11 will be Writing a XAML ribbon application for X11.

Background

Motivation

Although System.Windows.Forms applications are still alife, more and more existing application switch over to XAML and the majority of new applications are built with XAML now on the Microsoft® Windows® platform.

Even though XAML introduces new problems to the developer like

  • redundant and verbose language syntax,
  • low efficiency causes higher processing costs,
  • designed for hierarchical models, not for relational models, it requires extra effort to express overlapping (non-hierarchical) relationships,
  • usage of namespaces makes handling and interpretion difficult and
  • critical ambiguities of a supposedly simple language

it has - among a lot of others - three very serious advantages. XAML

  • is platform-independent,
  • is plain text and easily to maintain and
  • forces a clear separation and loose coupling of GUI and business logic.

Since a good documentation can minimize the effect of the drawbacks, the advantages are even more serious. Why not use XAML for X11 application development too and avoid rather frustrating initial experience (because of the fairly steep learning curve) with a good tutorial? O.K. - let's go!

Concept

To introduce XAML to X11 application development some challenges have to be mastered:

  1. The GUI application framework must support dynamic creation of controls and other framework elements.
  2. The XAML syntax should be as near to the Microsoft® original as reasonable. This shall
    • allow the transfer of acquired knowledge and written XAML code between X11 and Windows® and
    • support the separation of GUI and business logic to write platform independent code.
  3. Framework elements, defined within XAML, must also be accessible through C# code. This is required to enable the property change notification mechanism for notification from ViewModel to View.
  4. The GUI application framework must support appropriate reflection methods to enable the property change notification mechnism for notification from View to ViewModel.

The approaches to master these challenges  are:

  1. An XAML wrapper around the Roma Widget Set. XAML wrapper and Xrw are written entirely in C# and can take usage of the dynamic language features.
  2. A custom XAML interpreter. This enables easy adopting to the desired syntax and functionality as near to the Microsoft® original as reasonable.
  3. An individually created XAML preprocessor, that dynamically generates that one part of partial classes, that is associated with the definitions made within XAML.
  4. The implementation of suffisticated generic reflection methods for the XAML wrapper around Xrw.

To evaluate these approaches a basic XAML based dialog application should be implemented. The sample application is written with Mono Develop in C#. It is based upon Mono Develop's fetaure to run user defined commands before code compilation to run the XAML preprocessor.

Focus

This article shall demonstrate that

  • a window with some controls can be defined with XAML and
  • click events can be connected to buttons with XAML.

Using the code

The sample application was written with Mono Develop 2.4.1 for Mono 2.8.1 on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop. Neither the port to any older nor to any newer version should be a problem. The sample application's solution consists of two projects (the complete sources are provided for download):

  • XamlDialogApp contains the source code of the sample application.
  • XamlPreprocessor contains the source code of the XAML preprocessor.

The sample application is also tested with Mono Develop 3.0.6 for Mono 3.0.4 on OPEN SUSE 12.3 Linux 64 bit DE and GNOME desktop, IceWM, TWM und Xfce.

The only difference between the 32 bit and the 64 bit solution is the definition of some X11 specific data types, as already described in the Programming Xlib with Mono develop -Part 1: Low level (proof of concept) article.

The Xlib/X11 window handling is based on the X11Wrapper assembly version 0.7, that defines the function prototypes, structures and types for Xlib/X11 calls to the libX11.so. This assembly has been developed for the Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept) project and has been advanced during the Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Basics project.

The GUI framework is based on the Xrw assembly version 0.7, that defines the widgets/gadgets and its wrapper classes used within the XAML code (that should be as near to the Microsoft® original as reasonable). This assembly has been developed during the Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Basics project.

Advice: To use the class library documentation shortcut (F1) from MonoDevelop, the "mono-tools" package has to be installed.

The image shows the sample application with XrwTheme.GeneralStyle.WinMidori.

The sample application's business functionality consists of

  • the decoding of Base64 coded text (*.txt) to binary image files (*.decoded.bmp),
  • the encoding of binary image files (*.bmp) to Base64 coded text (*.encoded.txt) and
  • the opening of the recently encoded file by the system's default text editor

to demonstrate a practical use case for a dialog interface application.

The default image format (that means the default file name extension) is BMP. Other file formats should work as well, but require to change the file name extension afterwards.

The Windows version, developed in parrallel to realize XAML code as near to the Microsoft® original as reasonable, uses System.Convert.FromBase64String() and System.Convert.ToBase64String(). Because of the buggy implementation of these methods in Mono, the Linux/Unix versions of this sample application use X11.AlternativeBase64.Decode() and X11.AlternativeBase64.Encode() instead.

Step by step instruction

Project setup

First of all we need a new empty C# project. No additional features are required. MONO / .NET 3.5 or higher is recommended as runtime version.

The project references must include the standard packages System, System.Core, System.Drawing, System.Xml and the assemblies (or projects) X11Wrapper and Xrw. There are seven initial files required for the project, that can be named arbitary. To be as near to the Microsoft® original as reasonable, recommended are

  • App.xaml for the application's class XAML definition,
  • App.xaml.cs for the application's class (manual) implementation / code behind,
  • MainModel.cs for the MVVM data Model of the main view,
  • MainView.xaml for the (dialog window) main view's class XAML definition,
  • MainView.xaml.cs  for the (dialog window) main view's class (manual) implementation / code behind,
  • MainViewModel.cs for the MVVM ViewModel of the main view and
  • the application's icon file.

And finally the XAML preprocessor must be included into the project. This is done by a user defined command, executed before compilation.

The preprocessor is part of the solution and situated at '../XamlPreprozessor/bin/Debug/XamlPreprozessor.exe' relative to the XamlDialogApp project folder. The location can be changed without any drawback. The current working directory of the command to execute must be set to the XamlDialogApp project folder, otherwise the preprocessor might examine the wrong folder.

Currently the preprocessor doesn't examine sub-folders. But this is O.K. for this sample.

Application file content

The XAML (App.xaml)

The first XAML file to take a look at is App.xaml.

<Application    x:Class="XamlDialogApp.App"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                StartupUri="MainView.xaml"
                Style="WinMidori">
    <!-- Supported styles are: WinClassic, WinLuna, WinRoyale, WinMidori, Gtk2Clearlooks -->
    <Application.Resources>
    </Application.Resources>
</Application>

Except the attribute Style="WinLuna", the XAML code is fully Microsoft® compatible. Alternatively the style can be set within App.xaml.cs to avoid this incompatibility.

The Application will be defined by:

  • The root node is named Application. This node is mandatory. The XAML processing relies on the node name Application.
  • The x:Class attribute defines the namespace name (XamlDialogApp) and class name (App) of the application. This attribute is mandatory. It is strictly recommended to define namespace and class. The XAML processing relies on the attribute name x:Class. The attribute name x:Class is a XAML extension, defined within the http://schemas.microsoft.com/winfx/2006/xaml namespace.
  • The xmlns and xmlns:x attributes are currently not evaluated and part of the XAML code for Microsoft® compatibility only.
  • The StartupUri attribute defines the XAML file URI of the startup UI, that is MainView.xaml for this sample. This attribute is mandatory. The XAML processing relies on the attribute name StartupUri.
  • The Style attribute defines the theme, Xrw shall use to display the UI. This attribute is optional.
  • The Application.Resources node is currently not evaluated. This attribute is optional.

The code behind (App.xaml.cs)

The corresponding C# code behind file is App.xaml.cs.

using System;
using Xrw;
using XrwXAML;

namespace XamlDialogApp
{
    /// <summary>This is the application's main class.</summary>
    /// <remarks>It must be inherited from XrwXAML.Application and
    /// contain a Main method to start the application.</remarks>
    public partial class App : XrwXAML.Application
    {
        /// <summary>The application starter method.</summary>
        /// <returns>Zero on success, nonzero otherwise.<see cref="System.Int32"/></returns>
        public static int Main ()
        {
            // Delegate the hard work to the base class.
            return Main (System.Reflection.Assembly.GetExecutingAssembly());
        }

        /// <summary>The public constructor.</summary>
        public App ()
        {
           this.InitializeComponent();
        }
        
    }
}

Mind the fact that the App class is defined as partial class! This is important, because the XAML preprocessor will generate the second part of the App class based on the XAML code in App.xaml.

Main view file content

The XAML (MainView.xaml)

The second XAML file to take a look at is MainView.xaml.

<Window         x:Class="XamlDialogApp.MainView"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:src="clr-namespace:XamlDialogApp"
                DataContext="src:MainViewModel"
                Name="MainWindow" Title="XAML dialog application"
                Width="650" Height="400" Icon="XrwIcon16.bmp">
    <Window.Resources>
    </Window.Resources>
    <Grid Name="MainGrid">
        <Grid.Resources>
            <!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
        </Grid.Resources>
        <Grid.Datacontext>
            <!-- <Binding Source="{StaticResource mainViewDataSource}"/> -->
        </Grid.Datacontext>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="12"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="20"/>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="12"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="12"/>
            <RowDefinition Height="28"/>
            <RowDefinition Height="12"/>
            <RowDefinition Height="28"/>
            <RowDefinition Height="50"/>
            <RowDefinition Height="28"/>
            <RowDefinition Height="12"/>
            <RowDefinition Height="28"/>
            <RowDefinition Height="12"/>
            <RowDefinition Height="68"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="12"/>
            <RowDefinition Height="32"/>
        </Grid.RowDefinitions>
        <TextBox Name="DecodeSourcePath" Grid.Column="1" Grid.Row="1" Text=""
                 BorderThickness="2" IsEnabled="false" />
        <TextBox Name="DecodeTargetPath" Grid.Column="1" Grid.Row="3" Text=""
                 BorderThickness="2" IsEnabled="false" />
        <Button Name="Decode" Grid.Column="3" Grid.Row="1" Grid.RowSpan="3" Click="Decode_Click" >
            <TextBlock Text="Decode Base64 to BMP/PNG" TextWrapping="Wrap"/>
        </Button>
        <TextBox Name="EncodeSourcePath" Grid.Column="1" Grid.Row="5" Text=""
                 BorderThickness="2" IsEnabled="false" />
        <TextBox Name="EncodeTargetPath" Grid.Column="1" Grid.Row="7" Text=""
                 BorderThickness="2" IsEnabled="false" />
        <Button Name="Encode" Grid.Column="3" Grid.Row="5" Grid.RowSpan="3" Click="Encode_Click" >
            <TextBlock Text="Encode BMP/PNG to Base64" TextWrapping="Wrap"/>
        </Button>
        <Label Name="EncodeData" Content="" Grid.Column="1" Grid.Row="9" Grid.RowSpan="2"
              BorderThickness="2" />
        <Button  Name="Open" Grid.Column="3" Grid.Row="9" Click="OpenResult_Click" >
            <TextBlock Text="Open result in a text editor" TextWrapping="Wrap"/>
        </Button>
        <Label Name="State" Content="O.K." Grid.Column="0" Grid.Row="12" Grid.ColumnSpan="5"
               BorderThickness="2" />
    </Grid>
</Window>

The complete XAML code is fully Microsoft® compatible.

The Window will be defined by:

  • The root node is named Window. This node is mandatory. The XAML processing relies on the node name Window.
  • The x:Class attribute defines the namespace name (XamlDialogApp) and class name (MainView) of the main view. This attribute is mandatory. It is strictly recommended to define namespace and class. The XAML processing relies on the attribute name x:Class. The attribute name x:Class is a XAML extension, defined within the http://schemas.microsoft.com/winfx/2006/xaml namespace.
  • The xmlns and xmlns:x attributes are currently not evaluated and part of the XAML code for Microsoft® compatibility only.
  • The xmlns:src attribute defines the namespace of local source code resources. This attribute is recommended, or mandatory if any attribute values use the prrefix src: as reference. The XAML processing relies on the suffix :src. The syntax of the attribute value must be clr-namespace:<namespace name>. The namespace, defined this way, can be referenced by attribute values via the prefix src:.
  • The DataContext attribute defines the default/fallback data context, that is src:MainViewModel for this sample. This attribute is optional. The DataContext attribute is currently not evaluated.
  • The Name attribute defines the class instance name, that can be used to identify the class instance uniquely. This attribute is mandatory. The class instance name must not be the same as the class type name!
  • The Title attribute defines the window title. This attribute is optional.
  • The Width attribute defines the initial window width. This attribute is optional.
  • The Height attribute defines the initial window height. This attribute is optional.
  • The Icon attribute defines the window icon. This attribute is optional.
  • The Window.Resources node is currently not evaluated. This node is optional.

The Window's root (widget geometry) manager control is a Grid and will be defined by:

  • The Name attribute defines the class instance name, that can be used to identify the class instance uniquely. This attribute is recommended, or mandatory if this class instance has to be accessible through C# code.
  • The Grid.Resources node is currently not evaluated. This node is optional.
  • The Grid.Datacontext node is currently not evaluated. This node is optional.
  • The Grid.ColumnDefinitions node joins the column definitions. This node is mandatory.
    - The Grid.ColumnDefinition node defines one grid column. This node is mandatory, at least once.
       - The Width attribute defines the grid column width. This attribute is mandatory. Positive integers are interpreted as width in pixel. The asterisk is interpreted as dynamic width.
  • The Grid.RowDefinitions node joins the row definitions. This node is mandatory.
    - The Grid.RowDefinition node defines one grid row. This node is mandatory, at least once.
       - The Height attribute defines the grid row height. This attribute is mandatory. Positive integers are interpreted as height in pixel. The asterisk is interpreted as dynamic height.
  • The Grid node can contain nodes, that define child controls, to be layed out by the grid.

The Grid control contains TextBox controls as children and they will be defined by:

  • The Name attribute defines the class instance name, that can be used to identify the class instance uniquely. This attribute is recommended, or mandatory if this class instance has to be accessible through C# code.
  • The Grid.Column attribute defines the zero-based column index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on column 0 inside a grid. The index must not exceed the available grid columns.
  • The Grid.Row attribute defines the zero-based row index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on row 0 inside a grid. The index must not exceed the available grid rows.
  • The Grid.ColumnSpan attribute defines the number of columns, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid columns. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid columns.
  • The Grid.RowSpan attribute defines the number of rows, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid rows. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid rows.
  • The Text attribute defines the text to display. This attribute is optional.
  • The BorderThickness attribute defines the width of the control's border. This attribute is optional.
  • The IsEnabled attribute defines the sensitivity of the control. This attribute is optional. The default is true.

The Grid control contains Button controls as children and they will be defined by:

  • The Name attribute defines the class instance name, that can be used to identify the class instance uniquely. This attribute is recommended, or mandatory if this class instance has to be accessible through C# code.
  • The Grid.Column attribute defines the zero-based column index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on column 0 inside a grid. The index must not exceed the available grid columns.
  • The Grid.Row attribute defines the zero-based row index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on row 0 inside a grid. The index must not exceed the available grid rows.
  • The Grid.ColumnSpan attribute defines the number of columns, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid columns. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid columns.
  • The Grid.RowSpan attribute defines the number of rows, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid rows. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid rows.
  • The Content attribute defines the text to display. This attribute is optional. A nestet TextBlock node can be used alternatively. The Microsoft® original implementation of this attribute can't provide line breaks.
    - The nested TextBlock node defines text to display. This node is optional. It can provide line breaks.
       - The Text attribute defines the text string. This attribute is mandatory.
       - The TextWrapping attribute defines the text wrapping. This attribute is optional.
  • The Click attribute defines the click event delegate. This attribute is optional. Currently the delegate must be defined inside the class code of the Window (code behind), this control is a child/grandchild of.

The Grid control contains Label controls as children and they will be defined by:

  • The Name attribute defines the class instance name, that can be used to identify the class instance uniquely. This attribute is recommended, or mandatory if this class instance has to be accessible through C# code.
  • The Grid.Column attribute defines the zero-based column index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on column 0 inside a grid. The index must not exceed the available grid columns.
  • The Grid.Row attribute defines the zero-based row index, the control has to be positioned inside a grid. The default value is 0. This attribute is recommended for a grid child, but mandatory for controls positioned not on row 0 inside a grid. The index must not exceed the available grid rows.
  • The Grid.ColumnSpan attribute defines the number of columns, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid columns. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid columns.
  • The Grid.RowSpan attribute defines the number of rows, the control has to span inside a grid. This attribute is optional for a grid child, but mandatory for controls spanning multiple grid rows. To omit this attribute or to set it to "0" or "1" are equivalent. The span must not exceed the available grid rows.
  • The Content attribute defines the text to display. This attribute is optional.
  • The BorderThickness attribute defines the width of the control's border. This attribute is optional.

The code behind (MainView.xaml.cs)

The corresponding C# code file is MainView.xaml.cs. It contains the Click delegates for all three Button controls.

using System;
using System.IO;

using X11;
using Xrw;
using XrwXAML;

namespace XamlDialogApp
{
    /// <summary>The main window of the application. This class must be derived from XrwXAML.Window.
    /// It must be a partial class. The second part of the class will be autogenerated and named
    /// '*.generated.cs'.</summary>
    public partial class MainView : XrwXAML.Window
    {
        
        /// <summary>The default constructor.</summary>
        public MainView ()
            : base (-1, -1)
        {
            // InitializeComponent () and InitializeComponentGenerated()
            // will be called after construction by generated code!
        }
        
        /// <summary>Process the "Decode" button click event.</summary>
        /// <param name="sender">The event source.<see cref="System.Object"/></param>
        /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
        private void Decode_Click(object sender, RoutedEventArgs e)
        {
            ...
        }
        
        /// <summary>Process the "Encode" button click event.</summary>
        /// <param name="sender">The event source.<see cref="System.Object"/></param>
        /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
        private void Encode_Click(object sender, RoutedEventArgs e)
        {
            ...
        }
        
        /// <summary>Process the "Open" button click event.</summary>
        /// <param name="sender">The event source.<see cref="System.Object"/></param>
        /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
        private void OpenResult_Click(object sender, RoutedEventArgs e)
        {
            if (!string.IsNullOrEmpty (EncodeTargetPath.Text))
                System.Diagnostics.Process.Start (EncodeTargetPath.Text);
        }
        
        /// <summary>Decode with Base64.</summary>
        /// <param name="sourceStream">The source to decode.<see cref="Stream"/></param>
        /// <param name="targetStream">The decoded target.<see cref="Stream"/></param>
        /// <param name="message">The message describing the success.<see cref="System.String"/></param>
        /// <returns>True on success, or false otherwise.<see cref="System.Boolean"/></returns>
        public bool DecodeBase64FromStream(Stream sourceStream, Stream targetStream, out string message)
        {
            ...
        }
        
        /// <summary>Encode with Base64.</summary>
        /// <param name="sourceStream">The source to encode.<see cref="Stream"/></param>
        /// <param name="targetStream">The encoded target.<see cref="Stream"/></param>
        /// <param name="message">The message describing the success.<see cref="System.String"/></param>
        /// <returns>True on success, or false otherwise.<see cref="System.Boolean"/></returns>
        public bool EncodeBase64FromStream(Stream sourceStream, Stream targetStream, out string message)
        {
            ...
        }
        
    }
}

Mind the fact that the MainView class is defined as partial class! This is important, because the XAML preprocessor will generate the second part of the MainView class based on the XAML code in MainView.xaml.

Preprocessor code generation

During the first compilation of the project, the preprocessor initially generates the partial class files App.generated.cs from App.xaml and MainView.generated.cs from MainView.xml. The Preprocessor writes log messages to the standard output, that can be inspected at the build output.

To go on with this sample, the initially generated files must be included into the project. Otherwise Mono Develop won't include the files into compilation and the generated parts of the partial classes App and MainView are missing. (That's why the very first compilation always generates errors.)

 

The generated *.generated.cs files are incompatible to the Microsoft® ones:

  • The Microsoft® ones are named *.g.cs.
  • The Microsoft® ones are hidden from the project( but the Mono ones mut be included into the project).
  • The file content is completely different.

Now the functionality can grow step by step and the compilation produces executable assemblies.

Points of Interest

Is it possible to create MVVM design pattern based X11 application with XAML? YES it is! And it's fun. Hence this will not be the last article about XAML programming for X11.

Since 29. October 2014 the article Writing a XAML ribbon application for X11 continues the XAML topic.
Since 23. November 2014 the article Writing a XAML application for X11 with massive data binding and zero code continues the XAML topic.
Since 02. February 2015 the article Writing a XAML calculator application for X11 continues the XAML topic.

History

The first version is from 10. October 2014.
The first reviewed version from 29. October 2014.
The second reviewed version from 23. November 2014.
The third reviewed version from 19. February 2015.

License

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

Share

About the Author

Steffen Ploetz
CEO Ploetz + Zeller GmbH
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
mrmike2-Feb-15 4:28
professionalmrmike2-Feb-15 4:28 
GeneralMy vote of 5 Pin
fredatcodeproject10-Oct-14 10:43
professionalfredatcodeproject10-Oct-14 10:43 

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.

Article
Posted 10 Oct 2014

Stats

11.3K views
251 downloads
7 bookmarked