Click here to Skip to main content
15,881,715 members
Articles / Web Development / HTML

Writing a XAML calculator application for X11

Rate me:
Please Sign up or sign in to vote.
4.67/5 (5 votes)
2 Feb 2015CPOL15 min read 22.5K   818   7  
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 reviews a XAML based application with basic menu, clipboard & validation features.

Download XamlCalcApp_X11 V1_32.zip / Download XamlCalcApp_X11_V2_32.zip
Download XamlCalcApp_X11_V1_64.zip / Download XamlCalcApp_X11_V2_64.zip
Download XamlCalcApp_Win7_V1.zip / Download XamlCalcApp_Win7_V2.zip

Introduction

This article is a case study, how to write a MVVM (Model View ViewModel) design pattern based X11 application (utilizing basic menu, clipboard, default validation, key binding and custom validator features) 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#.

This article continues the works Writing a XAML dialog application for X11, Writing a XAML ribbon application for X11 and Writing a XAML application for X11 with massive data binding and zero code. 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 more complex 'proof of concept' and checks out if and how it is possible bo create MVVM design pattern based X11 application with XAML.

Since this fourth attempt to use XAML for a X11 application development has been successful, further articles about XAML using the Roma Widget Set on X11 will follow certenly.

Background

The Motivation and the general Concept to use XAML for X11 application development are already explained in the Writing a XAML dialog application for X11 article.

Focus

While the first article Writing a XAML dialog application for X11 demonstrated, that with XAML

  • a window containing some controls can be defined and
  • click events can be connected to buttons,

the second article Writing a XAML ribbon application for X11 demonstrated, that with XAML

  • a window containing a ribbon command interface can be defined,
  • static window resources (this sample has a resource converter and a ModelView) can be defined,
  • a ModelView can be assigned to a control's data context as a static resource,
  • a resource converter can be applied to a control's property as a static resource,
  • commands can be bound to buttons via the "RelayCommand" approach,
  • control properties can be bound to the data context and
  • controls can be updated via the INotifyPropertyChanged interface

and the third article  Writing a XAML application for X11 with massive data binding and zero code demonstrated, that with XAML

  • massive data binding can provide a useful functionality and
  • an application with zero code behind can be defined,

this article shall demonstrate that with XAML

  • simple menus can be designed easily,
  • clipboard text exchange can be achieved and
  • data validation shows input errors, utilizing the built-in converter exception.

The updated version of this article shall demonstrate as well

  • custom binding validation and
  • keyboard shortcut binding.

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):

  • XamlCalcApp 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.8, 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.8, 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.

All images show the sample application in the same state: The number 12 is memorized (M) and should be multiplied with the next value (12 *), but the entered value (#4) is faulty - it can't be converted to a number. This is indicated by the red border around the TextBox.

The first image shows the sample application on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop.

The second image shows the sample application on OPEN SUSE 12.3 Linux 64 bit DE and Xfce.

The third image shows the sample application on Windows® 7 64 Bit Edition.

The sample application provides a simple but full functional calculator. It contains (top down) a simple menu bar with the one-level menus File and ?,  a TextBox that accepts keyboard input, two TextBlocks that show the memory state (M means a number is memorized) and the previously captured operator (e. g. 12 *) as well as multiple rows and columns of buttons including a row spannning button and a column spanning button.

All buttons have callbacks registered to their Click property to achieve the functionality (the Click property reduces the code and has no disadvantages for this simple project compared to the Command property and "RelayCommand" approach, discussed in the Writing a XAML ribbon application for X11 article).

The TextBox automatically examines the input for non-numerical characters and, if input can't be converted to a number, it displays a red border around the TextBox (as shown by the images). There is no auto-correction for typing errors to keep the sample application simple and demonstrate the data validation, utilizing the built-in converter exception, on X11 and Windows.

The sample application is based on the ideas behind the CodeProject article Calc# - An introduction to Gtk# by Priyank Bolia. But instead of a Gtk# indroduction, this article is intended to introduce XAML.

Walk through

The Project setup, Application file context (except the theme) and Preporocessor code generation steps are exactly the same as in Writing a XAML dialog application for X11. Please refer to this article, if a new solution shall be created from scratch.

Main view file context

The XAML (MainView.xaml)

First the XAML file with omitted button definition.

XML
<Window         x:Class="XamlCalcApp.MainView"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:src="clr-namespace:XamlCalcApp"
                xmlns:validators="clr-namespace:XamlCalcApp"
                Name="MainWindow" Title="XAML calculator application"
                Width="300" Height="320" Icon="XrwIcon16.bmp">
    <!-- ATTENTION: To set an application icon, a Resources.resx file must
                    be created for the project on the windows platform. -->
    <Window.Resources>
        <src:MainWindowViewModel x:Key="MainViewModel" />
        <!-- MainWindowViewModel : ViewModelCore<T>.ctr() automatically registeres
             itself as the initial Window.DataContext -->
    </Window.Resources>
    <Grid Name="MainGrid" Background="#E8E8E8" DataContext="{StaticResource MainViewModel}">
        <Grid.Resources>
            <!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.1*"/><!-- Left padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 1-2 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 2-3 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 3-4 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 4-5 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.1*"/><!-- Right padding. -->
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="22"/><!-- Menu bar. -->
            <RowDefinition Height="32"/><!-- Editor bar. -->
            <RowDefinition Height="18"/><!-- Info bar. -->
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="0.8*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.1*"/><!-- Bottom padding. -->
        </Grid.RowDefinitions>
        <Menu Name="MainMenu" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="9" >
            <MenuItem Name="MenuItemFile"  Header="File">
                <MenuItem Name="MenuItemFileCopy"   Header="Copy"
                          Click="MenuItemFileCopy_Click" />
                <MenuItem Name="MenuItemFilePaste"  Header="Paste"
                          Click="MenuItemFilePaste_Click" />
                <Separator/>
                <MenuItem Name="MenuItemFileEdit"   Header="Exit"
                          Click="MenuItemFileExit_Click" />
            </MenuItem>
            <MenuItem Name="MenuItemQmark" Header="?">
                <MenuItem Name="MenuItemQmarkHelp"  Header="Help"
                          Click="MenuItemQmarkHelp_Click" />
                <MenuItem Name="MenuItemQmarkAbout" Header="About"
                          Click="MenuItemQmarkAbout_Click" />
            </MenuItem>
        </Menu>
        <!-- Short hand syntax. Doesn't support custom validation.
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2"  BorderBrush="#CCCCCC"
                 Text="{Binding Path=CurrentValue, Mode=TwoWay,
                 UpdateSourceTrigger=PropertyChanged}" />
        -->
        <!-- Long hand syntax. -->
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
            <TextBox.Text >
                <Binding Path="CurrentValue" Mode="TwoWay"
                   UpdateSourceTrigger="PropertyChanged" />
            </TextBox.Text>
        </TextBox>
        <!-- Validation rule implementation for version 2 of XamlCalcApp.
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
          <TextBox.Text >
            <Binding Path="CurrentValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">                   
              <Binding.ValidationRules>
                  <validators:TextIsFloatingNumberValidationRule
                              ErrorMessage="The input must be a valid floating point number." />
              </Binding.ValidationRules>
            </Binding>
          </TextBox.Text>
        </TextBox>
        -->
        <TextBlock Name="Memory"           Grid.Row="2"  Grid.Column="1" Text="" />
        <TextBlock Name="Info"             Grid.Row="2"  Grid.Column="3" Text=""
                   Grid.ColumnSpan="7"     TextAlignment="Right" />

...

    </Grid>
</Window>

The complete XAML code is fully Microsoft® compatible.

Compared to Writing a XAML application for X11 with massive data binding and zero code and previous articles, only the enhancments and differences will be discussed.

The Application will be defined (in addition to the already known features) by:

  • The xmlns:validators attribute defines the namespace name used for custom validator classes (typically derived from ValidationRule abstract class). This attribute has been added to do some validation rule tests in conjunction with the custom TextIsFloatingNumberValidationRule class. Even if the TextIsFloatingNumberValidationRule class is provided by the sample project, this attribute is currently not fully implemented for X11.

The Menu 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 Menu control contains MenuItem 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 Header attribute defines the menu item display text. This attribute is recommended. Currently hotkey markup (e. g. "_File" for [Alt]+[f] hotkey) is not supported.
  • 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 TextBox will be defined (in addition to the already known features) by:

  • The TextAlignment attribute defines the horizontal alignment of the text to display. This attribute is optional.
  • The FontSize attribute defines the font size of the text to display. This attribute is optional.
  • The BorderBrush attribute defines the color of the control's border. This attribute is optional. The border is visible only if the BorderThickness is set to a value > 0. Currently named colors and HTML color names ("#RRGGBB") are supported.
  • The Text attribute defines the text to display. This attribute is is recommended. The syntax of the attribute can be either "<value>" for constant values or  {Binding <path>, ElementName=<control name>, Mode=<mode>, UpdateSourceTrigger=<Trigger>} for dynamic resources (shorthand notation). A dynamic data source must implement INotifyPropertyChanged. Now the Mode is implemented for OneWay, TwoWay and OneWayToSource binding modes. The UpdateSourceTrigger currently supports Default, LostFocus and PropertyChanged. Beside the shorthand notation for dynamic resources, now the explicit notation is supported as well: <TextBox ...><TextBox.Text ><Binding <BindingExpression> /></TextBox.Text></TextBox>. where <BindingExpression> can consist of  Path="<path>" Mode="<mode>" UpdateSourceTrigger="<trigger>".

There are three alternative TextBox definitions contained in MainView.xaml. The first one, commented out, contains a fully functional shorthand notation (but shorthand notation doesn't support custom validation). The second one, active, contains a fully functional explicit notation. The support of the explicit notation is new to the Xrw assembly version 0.8. The third one, commented out, contains an experimental explicit notation including a custom validator. This feature isn't fully implemented with the first version of XamlCalcApp. (It is proved - by the Windows version of the sample application - to be not helpful for the specific implementation of the calculator sample application's first version).

The TextBlock (it is similar to Label but supports automatic line break) will be defined (in addition to the already known features) by:

  • The TextAlignment attribute defines the horizontal alignment of the text to display. This attribute is optional.

Back to the Buttons now. This is the XAML file extract with the button definitions, ommitted above.

XML
...

    <Button Name="MemoryClear"               Grid.Row="4"  Grid.Column="1"
            Click="MemoryClear_Click"        Content="MC" />
    <Button Name="MemoryRecall"              Grid.Row="4"  Grid.Column="3"
            Click="MemoryRecall_Click"       Content="MR" />
    <Button Name="MemorySave"                Grid.Row="4"  Grid.Column="5"
            Click="MemorySave_Click"         Content="MS" />
    <Button Name="MemoryAdd"                 Grid.Row="4"  Grid.Column="7"
            Click="MemoryAdd_Click"          Content="M+" />
    <Button Name="MemorySubstract"           Grid.Row="4"  Grid.Column="9"
            Click="MemorySubstract_Click"    Content="M-" />
        
    <Button Name="EditDeleteLast"            Grid.Row="6"  Grid.Column="1"
            Click="EditDeleteLast_Click"     Content="←" />
    <Button Name="EditClearEntry"            Grid.Row="6"  Grid.Column="3"
            Click="EditClearEntry_Click"     Content="CE" />
    <Button Name="EditClearAll"              Grid.Row="6"  Grid.Column="5"
            Click="EditClearAll_Click"       Content="C" />
    <Button Name="CalculateDivide"           Grid.Row="6"  Grid.Column="7"
            Click="CalculateDivide_Click"    Content="/" />
    <Button Name="CalculateSqrt"             Grid.Row="6"  Grid.Column="9"
            Click="CalculateSqrt_Click"      Content="x ¹/²" />
        
    <Button Name="NumberSeven"               Grid.Row="8"  Grid.Column="1"
            Click="NumberSeven_Click"      ><TextBlock Text="7"  FontWeight="Bold" /> </Button>
    <Button Name="NumberEight"               Grid.Row="8"  Grid.Column="3"
            Click="NumberEight_Click"      ><TextBlock Text="8"  FontWeight="Bold" /> </Button>
    <Button Name="NumberNine"                Grid.Row="8"  Grid.Column="5"
            Click="NumberNine_Click"       ><TextBlock Text="9"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateMultiply"         Grid.Row="8"  Grid.Column="7"
            Click="CalculateMultiply_Click"  Content="*" />
    <Button Name="CalculateSquare"           Grid.Row="8"  Grid.Column="9"
            Click="CalculateSquare_Click"    Content="x ²" />
        
    <Button Name="NumberFour"                 Grid.Row="10" Grid.Column="1"
            Click="NumberFour_Click"        ><TextBlock Text="4"  FontWeight="Bold" /> </Button>
    <Button Name="NumberFife"                 Grid.Row="10" Grid.Column="3"
            Click="NumberFife_Click"        ><TextBlock Text="5"  FontWeight="Bold" /> </Button>
    <Button Name="NumberSix"                  Grid.Row="10" Grid.Column="5"
            Click="NumberSix_Click"         ><TextBlock Text="6"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateSubtract"          Grid.Row="10" Grid.Column="7"
            Click="CalculateSubtract_Click"   Content="-" />
    <Button Name="CalculateReciprocal"        Grid.Row="10" Grid.Column="9"
            Click="CalculateReciprocal_Click" Content="1/x" />
        
    <Button Name="NumberOne"                  Grid.Row="12" Grid.Column="1"
            Click="NumberOne_Click"         ><TextBlock Text="1"  FontWeight="Bold" /> </Button>
    <Button Name="NumberTwo"                  Grid.Row="12" Grid.Column="3"
            Click="NumberTwo_Click"         ><TextBlock Text="2"  FontWeight="Bold" /> </Button>
    <Button Name="NumberThree"                Grid.Row="12" Grid.Column="5"
            Click="NumberThree_Click"       ><TextBlock Text="3"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateAdd"               Grid.Row="12" Grid.Column="7"
            Click="CalculateAdd_Click"        Content="+" />
    <Button Name="CalculateResult"            Grid.Row="12" Grid.Column="9"
            Click="CalculateResult_Click"     ontent="=" Grid.RowSpan="3" />
        
    <Button Name="NumberZero"                 Grid.Row="14" Grid.Column="1" Grid.ColumnSpan="3"
            Click="NumberZero_Click"        ><TextBlock Text="0"  FontWeight="Bold" /> </Button>
    <Button Name="NumberDecimalDot"           Grid.Row="14" Grid.Column="5"
            Click="NumberDecimalDot_Click"  ><TextBlock Text="."  FontWeight="Bold" /> </Button>
    <Button Name="CalculateNegate"            Grid.Row="14" Grid.Column="7"
             Click="CalculateNegate_Click"    Content="+/-" />
...

The Button control syntax already included the possibility to have a TextBox child with the very first Xrw XAML version. But internally the TextBox child hasn't been represented by a TextBox control, because the Button control has been implemented based on XrwButton. Instead the TextBox child's Text property content has been assigned to the Button control's Content property content. Now a Button control always contains a TextBox child control, because the Button control is implemented based on XrwFrame. If there is an explicit TextBox child control defined in the XAML file, the default TextBox child control will be replaced by the explicit definition.

Limitations of the XAML syntax

X11/Xrw implementation:

  • The MenuItem control currently doesn't support hotkey markup (e. g. "_File" for [Alt]+[f] hotkey).
  • The MenuItem control nesting depth is currently limited to 2.
  • The Binding syntax currently doesn't support binding mode Mode=OneTime and trigger UpdateSourceTrigger=Explicit.
  • The Binding syntax doesn't support custom validators (with the first version of this article - the second version supports this feature).

Second version of XAML (MainView.xaml)

The XAML file is completed with key bindings.

XML
    ...
</Window.Resources>
<Window.InputBindings>
    <KeyBinding Command="{Binding NumberZero}" Key="NumPad0"/>
    <KeyBinding Command="{Binding NumberZero}" Key="D0"/>
    <KeyBinding Command="{Binding NumberOne}" Key="NumPad1"/>
    <KeyBinding Command="{Binding NumberOne}" Key="D1"/>
    <KeyBinding Command="{Binding NumberTwo}" Key="NumPad2"/>
    <KeyBinding Command="{Binding NumberTwo}" Key="D2"/>
    <KeyBinding Command="{Binding NumberThree}" Key="NumPad3"/>
    <KeyBinding Command="{Binding NumberThree}" Key="D3"/>
    <KeyBinding Command="{Binding NumberFour}" Key="NumPad4"/>
    <KeyBinding Command="{Binding NumberFour}" Key="D4"/>
    <KeyBinding Command="{Binding NumberFife}" Key="NumPad5"/>
    <KeyBinding Command="{Binding NumberFife}" Key="D5"/>
    <KeyBinding Command="{Binding NumberSix}" Key="NumPad6"/>
    <KeyBinding Command="{Binding NumberSix}" Key="D6"/>
    <KeyBinding Command="{Binding NumberSeven}" Key="NumPad7"/>
    <KeyBinding Command="{Binding NumberSeven}" Key="D7"/>
    <KeyBinding Command="{Binding NumberEight}" Key="NumPad8"/>
    <KeyBinding Command="{Binding NumberEight}" Key="D8"/>
    <KeyBinding Command="{Binding NumberNine}" Key="NumPad9"/>
    <KeyBinding Command="{Binding NumberNine}" Key="D9"/>
    <!-- There is no Key definition for '.' or ','! Only the numerical pad decimal dot works. -->
    <KeyBinding Command="{Binding NumberDecimalDot}" Key="Decimal"/>
    <KeyBinding Command="{Binding CalculateDivide}" Key="Divide"/>
    <KeyBinding Command="{Binding CalculateDivide}" Key="D7" Modifiers="Shift"/> <!-- GERMAN
        KeyBoard! -->
    <KeyBinding Command="{Binding CalculateMultiply}" Key="Multiply"/>
    <KeyBinding Command="{Binding CalculateMultiply}" Key="OemPlus" Modifiers="Shift"/> <!--
        GERMAN KeyBoard! -->
    <KeyBinding Command="{Binding CalculateSubtract}" Key="Subtract"/>
    <KeyBinding Command="{Binding CalculateSubtract}" Key="OemMinus"/>
    <KeyBinding Command="{Binding CalculateAdd}" Key="Add"/>
    <KeyBinding Command="{Binding CalculateAdd}" Key="OemPlus"/>
    <KeyBinding Command="{Binding CalculateResult}" Key="D0" Modifiers="Shift"/> <!-- GERMAN
        KeyBoard! -->
</Window.InputBindings>
<Grid ...

The Window.InputBindings node contains KeyBinding nodes and they will be defined by:

  • The Command attribute defines the "RelayCommand" to execute. This attribute is is mandatory. The syntax of the attribute is  {Binding <path>}.
  • The Key=<key> defines any value of the System.Windows.Input.Key enumeration. This statement is mandatory.
  • The [Modifiers=<modifiers>] defines any value of the System.Windows.Input.ModifierKeys enumeration. This statement is optional. The default/fallback value is System.Windows.Input.ModifierKey.None.

Limitations of the XAML syntax

X11/Xrw implementation:

  • The KeyBinding's Command currently supports ICommand properties of the current DataContext only.
  • The KeyBinding currently doesn't support CommandParameter and  CommandTarget.
  • The KeyBinding's Key requires localization of key codes.

Some desirable key bindings can't be defined, e.g. there is no System.Windows.Input.Key value for '.' or ','. Some key binding won't work on other keyboards than GERMAN. This is because of

  • the keyboard layout and
  • the System.Windows.Input.KeyConverter's KeyToKeySym() implementation.

It might be necessary to change / extend both, to get ENGLISH keyboards working perfectly.

The code behind (MainView.xaml.cs)

The corresponding C# code file of the main view is MainView.xaml.cs. It contains all the necessary structures and attributes to implement the value calculaton as well as all click handler code.

C#
/// <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, IView
{
    /// <summary>Provide the possible actions, that are to apply next.</summary>
    private enum   SecondArgumentAction
    {
        /// <summary>No specific action is to apply.</summary>
        None,
        /// <summary>Add a second argument to the last value.</summary>
        Add,
        /// <summary>Subtract a second argument from the last value.</summary>
        Substract,
        /// <summary>Multiply a second argument with the last value.</summary>
        Multiply,
        /// <summary>Divide the last value by second argument.</summary>
        Divide,
        /// <summary>Clear the current entry.</summary>
        Clear
    }

    /// <summary>Store a self-reference, that enables other classes to access this.</summary>
    private static MainView      _instance = null;

    /// <summary>Define the currently selected action.</summary>
    private SecondArgumentAction _operator = SecondArgumentAction.None;

    /// <summary>The last value.</summary>
    private double               _value = 0;

    /// <summary>The currently memorized value.</summary>
    private double               _memValue = 0;

    /// <summary>The language specific decimal delimiter.</summary>
    private string               _decimalDelimiter = 
        CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;

    /// <summary>The invariant culture number format.
    /// The number parsing relies on invariant culture.</summary>
    private NumberFormatInfo     _invariantNF = CultureInfo.InvariantCulture.NumberFormat;

    /// <summary>The default constructor.</summary>
    public MainView ()
        : base (-1, -1)
    {
        _instance = this;
        InitializeComponent ();
        // will be called after construction by generated code!
    }

    ...

    /// <summary>Process the "File.Copy" 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 MenuItemFileCopy_Click(object sender, RoutedEventArgs e)   
    {   
        Clipboard.SetText (Entry.Text);
    }

    /// <summary>Process the "File.Paste" 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 MenuItemFilePaste_Click(object sender, RoutedEventArgs e)   
    {   
        // Besause of the asynchronous processing of clipboard data(inter-application
        // message processing) a delegate to inject the result must be provided.
        Clipboard.GetText (this.ProcessClipboardPasteToEntry);
    }

    ...
}

The code block doesn't contain all the click handlers - they would mainly be repetitions. There is one thing, that should be poited out nevertheless: Clipboard text transfer.

The System.Windows static class Clipboard contains the methods SetText() and GetText() and they have equivalents within Xrw version 0.8 now. But it has been inpossible to implement the GetText() method fully Windows compatible (that means parameterless). Because of considerable differences between the monolithic design of Window's Desktop-Windows-Manager compared to the X11 Windows-Manager, the GetText() method expects a delegate to process the asynchronously provided clipboard result. The sample application implements ProcessClipboardPasteToEntry() to realize this (see source code above for the sample application's X11 version implementation).

The sample application's Windows version implements MenuItemFilePaste_Click() this way:

C#
/// <summary>Process the "File.Paste" 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 MenuItemFilePaste_Click(object sender, RoutedEventArgs e)
{
    string rawPastable = Clipboard.GetText();
    ProcessClipboardPasteToEntry(rawPastable);
}

The ProcessClipboardPasteToEntry() delegate of the sample application's X11 version and the ProcessClipboardPasteToEntry() method of the Windows version share exactly the same code:

C#
/// <summary>Handle the ClipboardGetResult event.</summary>
/// <param name="result">The clipboard get text result.<see cref="System.Object"/></param>
private void ProcessClipboardPasteToEntry (object result)
{
    if (result != null)
    {
        string rawPastable   = result.ToString ();
        string cleanPastable = "";

        for (int charIndex = 0; charIndex < rawPastable.Length; charIndex++)
        {
            if (char.IsDigit (rawPastable[charIndex]))
                cleanPastable += rawPastable[charIndex];
            else
            {    string s = rawPastable.Substring (charIndex, 1);
                if (_decimalDelimiter == s || "." == s)
                    cleanPastable += _decimalDelimiter;
                else
                    break;
            }
        }

        if (!string.IsNullOrEmpty (cleanPastable))
        {
            double d = 0;
            if (double.TryParse (cleanPastable, out d) == true)
            {
                Entry.Text = d.ToString ();
            }
        }
    }
}

Main view model file context

The only thing, the MainWindowViewModel.cs file contains beyond the initial state, is the CurrentValue property, the TextBox Entry is bound to.

C#
...

    #region Attributes
    
    ...
    
    /// <summary>The currently entered value.</summary>
    private string        _currentValue = 0;
    
    #endregion

...

    #region Properties
    
    ...

    /// <summary>Get or set the currently entered value.</summary>
    public string CurrentValue
    {    get
        {    return _currentValue;    }
        set
        {    if (_currentValue == value)
                return;
            
            _currentValue = value;
            RaisePropertyChanged("CurrentValue");
        }
    }
    
    #endregion

....

The binding of the CurrentValue property to the TextBox Entry's Text property is done in three steps:

  1. The static resource MainViewModel is bound to a MainWindowViewModel instance by the XML node <Window.Resources>.
  2. The Grid connects the static resource MainViewModel to it's DataContext property.
  3. The TextBox Entry binds the DataContext's CurrentValue property to it's Text property.
XML
...

    <Window.Resources>
        <src:MainWindowViewModel x:Key="MainViewModel" />
    </Window.Resources>

...

    <Grid Name="MainGrid" Background="#E8E8E8" DataContext="{StaticResource MainViewModel}">

...

        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
            <TextBox.Text >
                <Binding Path="CurrentValue" Mode="TwoWay"
                    UpdateSourceTrigger="PropertyChanged" />
            </TextBox.Text>
        </TextBox>

...

    </Grid>

First version of XamlCalcApp

There is no special functionality associated with the bound MainWindowViewModel's  CurrentValue property. But since it is of type System.Double and the TextBox Entry's Text property is of type System.String, the binding requires automatic type conversion. A failed type conversion throws an exception. For the TextBox Entry's Text "#4", as shown in the sample application images, the exception is:

System.Windows.Data Error: 7 : ConvertBack cannot convert value '#4' (type 'String'). BindingExpression:Path=CurrentValue; DataItem='MainWindowViewModel' (HashCode=44944057); target element is 'TextBox' (Name='Entry'); target property is 'Text' (type 'String') ...

This ecxeption is catched by the data binding and leads to a red border around the TextBox, indicating the typing error. Currently there is no auto-correction for erroneous entry to keep the sample application simple and demonstrate the data validation.

Next functionality

The next things, that could be implemented, are

  • a built-in auto-correction integrated into MainWindowViewModel's  CurrentValue property setter and
  • an advanced keyboard support, that simulates the button press events for [0] ... [9], [.], [+], [-], [*] [/] and [=].

Second version of XamlCalcApp

The definition of the CurrentValue property as a System.Double data type has one big drawback: Floating point numbers can't be entered. The second version of this sample application changes the CurrentValue property data type to System.String. But now the automatic type conversion is unnecessary (both, the TextBox Entry  and MainWindowViewModel properties are of type System.String) and typing errors don't throw a System.Windows.Data Error exception any longer (based on this, the TextBox Entry indicates the error by a red border around the TextBox).

Now there is a validation rule bound to the TextBox Entry and a custom validator is implemented instead:

XML
<TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
         TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
    <TextBox.Text >
        <Binding Path="CurrentValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">  
            <Binding.ValidationRules>
                <validators:TextIsFloatingNumberValidationRule
                            ErrorMessage="The input must be a valid floating point number." />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

This is the validator class. The error message text is injected via the ErrorMessage property from the XAML code.

C#
/// <summary>Implement a validation rule for character input.</summary>
public class TextIsFloatingNumberValidationRule : ValidationRule
{
    /// <summary>The message to display in case of an error.</summary>
    private string _errorMessage;

    /// <summary>Create a new instance of the TextIsFloatingNumberValidationRule class.</summary>
    public TextIsFloatingNumberValidationRule ()
    {
    }

    /// <summary>Get or set the message to display in case of an error.</summary>
    public string ErrorMessage
    {
        get { return _errorMessage; }
        set { _errorMessage = value; }
    }

    /// <summary>Perform validation checks on a value.</summary>
    /// <param name="value">The value from the binding target
    /// to check.<see cref="System.Object"/></param>
    /// <param name="cultureInfo">The culture to use in this
    /// rule.<see cref="CultureInfo"/></param>
    /// <returns>A System.Windows.Controls.ValidationResult
    /// object.<see cref="ValidationResult"/></returns>
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (cultureInfo == null)
            cultureInfo = CultureInfo.CurrentCulture;

        string localDecimalSeparator =
           cultureInfo.NumberFormat.NumberDecimalSeparator;
        string invarDecimalSeparator =
           CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator;
        
        string inputString = (value ?? string.Empty).ToString();
        for (int charIndex = 0; charIndex < inputString.Length; charIndex++)
        {
            char c = inputString[charIndex];
            if (!char.IsNumber(c) &&
               localDecimalSeparator[0] != c &&
               invarDecimalSeparator[0] != c)
                return new ValidationResult(false, this.ErrorMessage);
        }
        return new ValidationResult(true, null);
    }
}

This provides the same typing error notification funtionality for the second version of the sample application as well.

Main model file context

The MainModel.cs file remains in initial state. There is no functionality provided to the application by the Model.

Points of Interest

It was an interesting work to create this XAML application for X11, almost fully compatible with Microsoft®. The longer i have been involved with parallel development in X11 and Windows based on XAML (this is the fourth case study), the more i'm convinced of XAML. This applies in particular to the 100% compatible GUI definition and the savings of code lines to create the GUI.

Negative aspects, as the many static objects and properties XAML requires (e. g. tons of System.Windows.DependencyProperty instances), are increasingly fading into the background.

History

The first version of this article is from 2. February 2015.

This is the second version, frviewed and extended, from 16. February 2015.

License

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


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
-- There are no messages in this forum --