Laboratory Training 4

Development of GUI Applications

1 Training Tasks

1.1 Individual Task

With the tools of Windows Presentanion Fundation (WPF) subsystem, create a graphical user interface application that implements an assignment of the previous laboratory training. A new program should provide the following functions:

  • Reading data from a chosen XML document
  • Table representation of loaded data (DataGridView)
  • Representation of additional data in a separate table (if necessary)
  • Editing data (modification, adding, removing)
  • Sorting data based on certain criteria
  • Selection data by a certain rule and display in a separate window
  • Storing data in a new file

Program should use the previously created class library.

1.2 Use of dynamic Data Type

Create a class "A simple fraction". Implement overloading for operations +, -, *, and /. Create a static class that provides functions for getting arithmetic mean and product of array items. Use dynamic type. Test these functions using both array of doubles and array of simple fractions.

1.3 Working with the File System

In all subdirectories starting from the given folder, find and display information about all the files, which length is greater than the given value.

1.4 Working with Delegates

Implement the task 1.5 ("Roots of an Equation") of the second laboratory training. Apply an approach based on the use of delegates. Try to use anonymous delegate.

1.5 Using Lambda Expressions and LINQ

Create a static class that contains the following functions for working with lists of integers (positive and negative):

  • search for items, which are exact squares
  • sort by decreasing of absolute values

Implement two approaches: through use of lambda expressions and by using LINQ.

1.6 Working with Menus and Data Tables

Develop a program in which the user enters the size of a two-dimensional array of (m and n), puts the items in a table cells, and calculates the sum of the products of rows using the corresponding menu function. The table is represented by the DataGridView component. The sum should be displayed via label or using dialog box, depending on the option (CheckBox). Implement the main menu.

1.7 The Use of Graphics Features (Advanced Task)

Develop a program of graphing standard trigonometric functions. The user selects the desired function using RadioButton component, enters start and end of the interval, and receives graph after clicking the appropriate button.

2 Instructions

2.1 dynamic Type

The dynamic contextual keyword was introduced in C# 4.0. Initially, dynamic type was designed to operate with COM objects. The dynamic type allows creation of variables which types can be determined at runtime, unlike var keyword, which allows type identification at compile time. Therefore, variable can change its actual type:

dynamic x = 1;
Console.WriteLine(x.GetType().Name); // Int32
x = "A";
Console.WriteLine(x.GetType().Name); // String
x = 2.5;
Console.WriteLine(x.GetType().Name); // Double

Note: the GetType() method returns an object of System.Type class that encapsulates information about variable's type, in particular its name (the Name property).

The following example shows the implementation of generic paradigm based on the dynamic type. The Sum() method can be applied to arguments of different types (int, double, string) because these types allow application of + operator:

class Program
{
    static dynamic Sum(dynamic x, dynamic y)
    {
        return x + y;
    }

    static void Main(string[] args)
    {
        int i1 = 1;
        int i2 = 2;
        Console.WriteLine(Sum(i1, i2)); // 3
        double d1 = 1.1; 
        double d2 = 2.2;
        Console.WriteLine(Sum(d1, d2)); // 3.3
        string s1 = "dot";
        string s2 = "net";
        Console.WriteLine(Sum(s1, s2)); // dotnet
    }
}

We also can create class (e.g. Complex) that overloads + operator. Objects of this class also can be used as actual arguments:

public class Complex
{
    public double A { get; set; }
    public double B { get; set; }
    public static Complex operator+(Complex c1, Complex c2)
    {
        return new Complex { A = c1.A + c2.A, B = c1.B + c2.B };
    }
    public override string ToString()
    {
        return A + " " + B + "i";
    }
}

. . .

Complex c1 = new Complex { A = 1, B = 2 };
Complex c2 = new Complex { A = 3, B = 4 };
Console.WriteLine(Sum(c1, c2));

We can try to send arguments of types, which do not support + operator. In this case compiler does not show error messages:

object o1 = new object();
object o2 = new object();
Console.WriteLine(Sum(o1, o2));

But at runtime we'll get an exception:

Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 
Operator '+' cannot be applied to operands of type 'object' and 'object'

The disadvantages of dynamic types are some decreasing of program effectiveness and extra debug difficulties. Possible errors can be found at runtime, but not at compile time.

You can create array of dynamic items. Actually, items can be of different types:

dynamic[] arr = new dynamic[3];
arr[0] = 1;
arr[1] = 1.2;
arr[2] = "a";
//arr[2] = new object();
dynamic sum = 0;
foreach (dynamic d in arr)
{
    sum += d;
}
Console.WriteLine(sum); // 2.2a

2.2 Partial Classes and Methods

The version 2.0 of C# language provides so-called partial types, which can be split into several parts. For example:

partial class Partial
{
    public int x1 = 1;
}

class Program
{
    static void Main(string[] args)
    {
        Partial p = new Partial();
        Console.WriteLine(p.x1 + p.x2); // 3
    }
}

partial class Partial
{
    public int x2 = 2;
}

Most often separate parts of the class are placed in different files. It allows automatic generation of some parts. Now the automatic generation and manual development can take place in different files. However, the location of the class in multiple files can cause some problems, because we some of the class parts in different files can disrupt normal class functioning.

Since version 3.0, C# supports so called partial methods that are associated with partial classes. A partial method is a method declared in one piece and implemented in the rest of the class. Declaration of the partial method similar to declaration of abstract methods, but instead of abstract keyword, the partial keyword is used:

partial class SomeClass
{
    partial void SomeFunc(int k);
    public void Caller()
    {
        SomeFunc(0);
    }
}

// The rest of the class, perhaps in another file:
partial class SomeClass
{
    partial void SomeFunc(int k)
    {
        // implementation
    }

}    

Using partial methods allows different implementations depending on the context of class usage. Since the implementation of the method may be missed, the resulting type must be void; parameters with the out specifier cannot be declared. Partial methods are always private.

2.3 Working with the File System

The System.IO namespace provides the ability to work not only with the contents of files, but also with the file system as a whole. The DirectoryInfo class implements the ability to work with folders. To create an object of this class, the (full or relative) path to the file should be passed as a parameter of the constructor. For example:

DirectoryInfo dir = new DirectoryInfo("C:\\Users");
DirectoryInfo currentDir = new DirectoryInfo("."); // Project folder (current)

The DirectoryInfo class contains methods for creation of a new folder (Create()), deleting the folder (Delete()), receiving an array of files specified folder (GetFiles()), receiving an array of subdirectories (GetDirectories()), as well as properties for working with attributes (Attributes), verification of the existence of the folder (Exists), for getting and setting a full path to the folder (FullName), etc.

Working with individual files is carried out through the FileInfo class. Its object can be created similarly. You can create a new text file using CreateText() method.

FileInfo file = new FileInfo("New.txt");
file.CreateText();    

There are also Create() and Delete() methods. The property Directory returns the folder where the file is located. The Attributes, Exists, and FullName properties are similar to ones in DirectoryInfo class. The work with some of these methods and properties can be illustrated by the following example:

using System;
using System.IO;

namespace FifthhLab
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Enter the name of the folder you want to create:");
            string dirName = Console.ReadLine();
            DirectoryInfo dir = new DirectoryInfo(dirName);
            // Create a new folder:
            dir.Create();
            // Create a new file in the new folder: 
            FileInfo file = new FileInfo(dir + "\\temp.txt");
            file.Create();
            // Displays a list of files:
            FileInfo[] files = dir.GetFiles();
            foreach (FileInfo f in files)
            {
                Console.WriteLine(f);
            }
        }
    }
}

2.4 Delegates

An important part of modern programming practices is the use of callback and messages (notifications). To implement callbacks and messages, C and C++ languages provide function pointers. This approach carries with it some drawbacks because it is not type-safe. This can produce errors that are not detected at the stage of compilation.

C# provides so called delegates - reference type objects, which refer to methods. The usage of delegates is very close to pointers to functions in C++ language, but C# delegates are managed objects. CRL guarantee that delegate points to an admissible object. There are two domains of usage delegates in C#:

  • callback
  • event handling

Delegates allow invocation of methods, which addresses are defined at run-time. All delegates are derived from System.MulticastDelegate type. Delegate stores method signature. Delegate instance allows link to particular method that have appropriate signature. The method signature consists of returning type and types of arguments.

Here is an example of delegate declaration:

delegate string MyDelegate(int x);

Delegate can be declared in namespace, as well as within class body. The standard syntax is used for method call; just instead of method name the instance of delegate is placed. To initialize delegate with particular method, you should use the following syntax:

delegate-type name = new delegate-type (method);

The method can belong to any class, only appropriate types (resulting type, arguments types) must correspond to ones in delegate declaration.

In the following example, class Solver is used for solving of an equation using bisection method. Delegate allows generic description of the left part of an equation:

using System;

namespace DelegatesTest
{
    public class Solver
    {
        public delegate double LeftSide(double x);

        public static double Solve(double a, double b, double eps, LeftSide f)
        {
            double x = (a + b) / 2;
            while (Math.Abs(b - a) > eps)
            {
                if (f(a) * f(x) > 0)
                {
                    a = x;
                }
                else
                {
                    b = x;
                }
                x = (a + b) / 2;
            }
            return x;
        }
    }
}

The next class uses previous one for solving a particular equation.

using System;

namespace DelegatesTest
{
    class Program
    {
        public static double F(double x)
        {
            return x * x - 2;
        }

        static void Main(string[] args)
        {
            Solver.LeftSide ls = new Solver.LeftSide(F);
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, ls));
        }
    }
}

You can omit explicit creation of instance of delegate type. Such instance will be created automatically:

using System;

namespace DelegatesTest
{
    class Program
    {
        public static double F(double x)
        {
            return x * x - 2;
        }

        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, F));
        }
    }
}

The version 2.0 of C# language introduces anonymous methods. Such methods allow create delegate-type object on the spot. The following syntax is used:

delegate(argument_list) { function_body }

The previous example can be simplified:

using System;

namespace DelegatesTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Solver.Solve(0, 2, 0.000001, 
                              delegate(double x) { return x * x - 2; } ));
        }
    }
}

Delegates allow composition. Several delegates that refer to different methods can be joined into single one and then can be executed in a row. You can add methods to the chain of execution using "+" or "+=" operators. The "-" operator allows remove particular method from a chain.

2.5 Events

The implementation of GUI applications is based on the mechanism of receiving and processing events. The entire program consists of initialization (registration of visual controls) and the main loop of receiving and processing events. The examples of events are moving or clicking the mouse, keyboard input, etc. Every registered visual control can receive events that relate to it, and perform the functions that process these events. The event processing mechanism in C# represents a model "publisher – subscriber" where class publishes an event that it can initiate, and any class can sign up for this event. When initiating events, an environment informs all the subscribers about the occurrence the event.

The method called when event occurs is determined by the delegate. It is necessary that a delegate taking two arguments. These arguments must represent two objects: one that initiated the event (publisher), and information object of a class derived from EventArgs class of .NET.

The special event keyword is used to describe the elements of the delegate type. You can add one or several even handlers using += operator.

Events help us to split system functionality into independent parts. Assume that some class allows us to get several positive integers within a given range. Another class applies two actions to each number: it checks whether this integer is prime number and whether it is divided into 7:

using System;

namespace FifthLab
{
    // Delegate for representing event:
    public delegate void NumberEvent(int n);

    public class NumbersChecker
    {
        // Delegate-type field:
        public event NumberEvent numberEvent;

        // Method that raises event for each random number:
        public void GetNumbers(int min, int max, int count)
        {
            Random rand = new Random();
            for (int i = 0; i < count; i++)
            {
                int n = rand.Next();
                n %= (max - min + 1);
                n += min;
                numberEvent(n);
            }
        }
    }

    class Program
    {
        // Event handler:
        static void PrintIfPrime(int n)
        {
            bool isPrime = true;
            for (int k = 2; k * k <= n; k++)
            {
                if (n % k == 0)
                {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime)
            {
                Console.WriteLine("Prime number: {0}", n);
            }
        }

        // Another event handler:
        static void IsDividedIntoSeven(int n)
        {
            if (n % 7 == 0)
            {
                Console.WriteLine("{0} is divided into 7", n);
            }
        }

        static void Main(string[] args)
        {
            NumbersChecker nc = new NumbersChecker();
            nc.numberEvent += PrintIfPrime;
            nc.numberEvent += IsDividedIntoSeven;
            nc.GetNumbers(min: 10, max: 100, count: 40);
        }
    }
}

2.6 Concepts of Functional and Declarative Programming

Functional programming is a programming paradigm, whereby the software is presented as a set of functions (without conditions and variables). Functions can be called one from the other, and thus realized the so-called closure and recursion.

Closure is a special kind of function that is defined in the body of another function. The inner function is created each time it executed. The nested function contains references to local variables of the external function. Each time the invocation of the external function, a new instance of the internal function with new references to variables is created.

Recursion is a mechanism of function invocation carried out from this function body (directly or indirectly).

Such languages as LISP, XQuery, F#, can be classified as functional programming languages.

Declarative programming is a paradigm whereby you describe results, which should be received instead of describing the sequence of actions that lead to this result. Examples of declarative languages are HTML, SQL (domain-specific languages), as well as and the logic programming language Prolog, and other languages that allow to define constraints. A large number of declarative languages is based on XML.

The latest versions of the C# language (starting from 3.0) are actually hybrid, because they support not only imperative, object-oriented and generic programming, but also functional and declarative programming.

2.7 The Implementation of Functional and Declarative Programming in C#

2.7.1 Lambda Expressions

Functional programming in C# is implemented using the so-called lambda expressions. Lambda expression is a form of representation of anonymous methods. As previously mentioned, you can create anonymous methods that implement some delegate. For example, to list selected elements by the certain feature, you can call the FindAll() method, which requires the so-called predicate (a Boolean-valued function) as a parameter. The FindAll() method returns a new list. You can specify the following implementation, which is based on the creation of anonymous method:

var a = new List<int> { 1, 3, 6, 7, 8, 9 };
// Get the list of even elements:
var b = a.FindAll(delegate(int i) { return i % 2 == 0; });

Lambda expression allows you to implement closure in a more compact way:

var b = a.FindAll(i => i % 2 == 0);

In the following example, we calculate the number of even list items:

int k = a.FindAll(delegate(int i) { return i % 2 == 0; }).Count;

In the following example, the ConvertAll() method is used for replacing of items:

var a = new List<int>(){ 1, 2, 3 };
var x = 3;
var b = a.ConvertAll(elem => elem * x); // closure
foreach (var k in b)
{
    Console.WriteLine(k); // 3, 6, 9
}

Inside the lambda expression, you can determine the list of parameters (before "=>") and statements that build the function body (after "=>"). If you have more than one parameter, you need to use brackets. You can also add compound operators {} in a function body.

In the latest versions of C# (starting with 7.0), the syntax of lambda expressions can be used to define methods, specifically constructors, destructors, overridden virtual methods, and within definition of properties and indexers, if the corresponding programming block can be implemented as a single expression. For example:

class ExpressionBodiedTest
{
    private int a;
    // Constructor:
    public ExpressionBodiedTest(int a) => this.a = a;
    // Overridden method:
    public override string ToString() => a + "";
    // Property:
    public int A
    {
        get => a;
        set => a = value;
    }
}

2.7.2 LINQ

The so-called LINQ technology (Language Integrated Query) defines a set of operators to create queries that are translated into a sequential call of specific methods. From a user perspective, LINQ provides opportunities for inclusion statements similar to SQL-queries. For example, the problem of finding even the elements could be resolved as follows:

var a = new List<int> { 1, 3, 6, 7, 8, 9 };
var c = from i in a where i % 2 == 0 select i;

In this example, we specify a rule of filling the resulting sequence. For example, items are replaced by their squares:

var c = from i in a where i % 2 == 0 select i * i;

The result of the query is a sequence of some type that implements the IEnumerable<> interface. You can apply the foreach construct to this sequence:

foreach (int x in c)
{
    Console.Write(x + " ");
}

You can also use other LINQ statements: join, on, equals, into, orderby, ascending, descending, group.

Using LINQ is most effective in database applications.

2.8 GUI Application Development using Windows Presentation Foundation Technology

2.8.1 Main Concepts

Windows Presentation Foundation (WPF) is a new graphical presentation subsystem that can be used for creation of desktop applications. It is available since NET Framework 3.0. WPF is a modern alternative to the Windows.Forms library. The main concept of WPF is the separation of application presentation (view) from the code. The main features of WPF are following:

  • declarative approach to the description of the GUI elements
  • use of vector graphics for drawing components
  • use of styles and templates of graphical user interfaces design

At the level of direct rendering WPF is based on the use of DirectX.

2.3.2 XAML

The XAML language (eXtensible Application Markup Language) is XML-based markup language, which is designed to declaratively describe applications. The most effective use of XAML is description of user interface components, determining the properties of these components and binding components with event handlers. XAML allows to describe all visual objects WPF. In this example we create a main window with container (Grid) inside:

<Window x:Class="FirstXAML.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FirstXAML"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
 
    </Grid>
</Window>

Keep in mind that most components that are not containers can hold the only component inside. Therefore, if you want to group several elements, you should place containers into some component, and then you can add necessary child elements. The following code creates a button that is included into to a panel (Grid):

<Grid>
    <Button Height="72" Width="160" Content="Click Me" Margin="138,60,0,0" Name="ButtonClickMe" />
</Grid>

The Margin property defines the location relative to the container element. You can set the padding according to the left, top, right and bottom sides of the container. The Content property determines the content of some component. In the simplest case it determines the text inside buttons, labels or other similar components.

If you design WPF application in the Visual Studio environment, the XAML code is generated automatically as you add components to the form. Setting properties can be performed whether in the Properties Properties, or directly in the XAML code.

2.3.3 Layout

Design of a user interface application includes the organization of content, namely positioning and setting up the necessary items according to some rules. This process is called layout.

In WPF layout is implemented using a variety of containers. The WPF window can only contain the only child element. In most cases, this is some kind of container. Container, in turn, can hold several GUI elements, as well as other containers. The WPF layout is determined by container type. WPF Containers are panels, which determine rules of allocation of child GUI items. Container classes are derived from the abstract class System.Windows.Controls.Panel. The following classes are used to layout GUI components:

  • Grid places the elements in rows and columns according to an invisible table; Grid allocates items to the invisible grid of rows and columns; one grid cell holds one element which in turn can be another container in which you can create another group of controls;
  • UniformGrid, unlike the Grid, requires the installation of only the number of rows and columns and forms cells of the same size that occupy all the available space of the window (page) or the element of the outer container;
  • StackPanel – places items in horizontal and vertical stacks; This container is often used to organize small areas of a larger and more complex window;
  • WrapPanel – places controls in an accessible space, one line or column;
  • DockPanel – places controls on one of its outer edges;
  • Frame – similar to StackPanel, but is the best way to package content for page transitions.

Grid is the most powerful container in WPF. Most of what you can do with other layout containers can be done using Grid. Grid is the perfect tool for splitting a window (page) into smaller areas that can be managed using other panels. Such a division into small areas is carried out using the definitions of columns and rows:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="240*" />
        <ColumnDefinition Width="263*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="140*" />
        <RowDefinition Height="171*" />
    </Grid.RowDefinitions>
</Grid>

WrapPanel, depending on the Orientation arranges the controls horizontally (Horizontal) or vertically (Vertical), filling the current panel size. The horizontal arrangement of the controls may involve the transfer to the next line, and the vertical arrangement may involve the transfer to the next column.

The DockPanel panel docks child controls to one of its sides, depending on the value of the Dock property, which can take values Left, Right, Top, or Bottom. So if the control is attached to the top of the DockPanel, it stretches and will occupy the entire width of the panel, taking the height that is specified by the MaxHeight parameter.

Frame is a content management element that allows you to navigate to and display content. Frame can be placed inside other content, like other controls and elements. Content can be any type of object. Usually Frame is used to package the content of the page.

2.3.4 Basic WPF Controls and their Use

The WPF Visual Component Library provides typical controls: Button, Label, TextBox, ComboBox, RadioButton, etc. The differences relate primarily to properties and their application. Buttons and labels use Content instead of Text.

The TextBox control usually stores one line of some text. If you want to create a multi-line view, the TextWrapping property should be assigned the Wrap value. For You can specify the minimum and the maximum number of rows using the MinLines and MaxLines properties. You can use the new line character ('\ n') to jump to a new line inside the text.

The ListBox and ComboBox controls are used to work with lists. The ListBox control saves each added object in its collection. In this case, ListBox item can store not only strings, but any arbitrary element. The ComboBox control is similar to ListBox, but it uses a dropdown list and allows user only select one item from the list.

WPF has controls that use value ranges. This is ScrollBar, ProgressBar and Slider. You can use properties Value (the current value of the control), Minimum and Maximum of these controls. ScrollBar is a control that provides a bar with a rectangle whose position corresponds to a certain value. The ProgressBar control shows the progress of a long task. The Slider control is used to determine the numeric value by moving the slider on the scroll bar.

WPF also allows you to work with drop-down and pulldown menus. Use of menus will be considered later in the samples programs.

2.3.5 Data Binding in WPF

For displaying data, WPF uses data binding technology. For data aware components (like DataGrid), you can define the data source (ItemsSource). These may be, for example, lists, or some other collections. In addition, the data binding is used to bind data between the property value of some object and the property value of the control. Data binding can be done in the XAML code. For example, we associate a table column with the Name property:

<DataGridTextColumn Header="Name" Binding="{Binding Name}" />

Data binding can be also implemented in the C# code.

2.3.7 Development of WPF Application in Visual Studio Programming Environment

In order to create a new WPF application in the Visual Studio environment, you should create a new project by selecting the WPF Application template in the New Project template window. It is advisable to change the default project name (WpfApp1) to some meaningful name. Suppose the name FirstWpf was entered.

Now you can watch a window divided into two parts. The top part contains a form designer with the main window of the future application. At the bottom, there is a subwindow for editing the appropriate XAML code. This code will be stored in a .xaml file (MainWindow.xaml by default) and describes the main window and its elements. By default, the text will be as follows:

<Window x:Class="FirstWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FirstWpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        
    </Grid>
</Window>

The C# source code is generated simultaneously in automatic way (file MainWindow.xaml.cs):

using System;
using System.Collections.Generic;
using System.Linq; 
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace FirstWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

New elements of the graphical user interface can be added by visual tools by dragging the corresponding elements from the Toolbox subwindow, or by editing the XAML code. For example, if you add a button (Button) to the window from the Toolbox subwindow, it will be located in the place indicated by the mouse cursor. For example, if coordinates 320, 200 relative to the upper left form of the corner are chosen, then the following line will be added:

<Button Content="Button" HorizontalAlignment="Left" 
        Margin="320,200,0,0" VerticalAlignment="Top"/>

For example, if we want to place the button in the middle of the form and change the text, this can be done using the Properties subwindow by editing the properties, as well as by direct editing the XAML code. For example, the line between the <Grid> and </Grid> tags can be changed to this one:

<Button Content="Click me" HorizontalAlignment="Left" 
        Margin="320,200,0,0" VerticalAlignment="Top"/>

Now the button with the text "Click me" is located in the middle of the form. the button can be resized using mouse. In this case, new properties will be added to the button description:

<Button Content="Click me" ... Height="50" Width="120"/>

You can also change the title of the window in two ways – through the Properties submenu, or in the XAML code (Title property).

Now you can add an event handler associated with the push of a button. This can be done, for example, double-clicking on the button. The MainWindow class code within MainWindow.xaml.cs file now will contain a new method:

...
public partial class MainWindow : Window
{
    ...

    private void Button_Click(object sender, RoutedEventArgs e)
    {

    }
}

Corresponding reference will be automatically added to the description of the button in the XAML file:

<Button Content="Click me" ... Click="Button_Click" />

These steps can be done manually without the help of visual tools.

Now you can add some code to the body of Button_Click() method:

private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Thanks!"); // we add this line
}    

After starting the program the dialog box with the text "Thanks!" appears on the middle of the screen.

2.3.8 Using WPF Graphical Tools

From the beginning, WPF was announced as a graphical subsystem. Unlike previous libraries, the drawing ideology is built not on calling graphic functions, but on the addition of graphic primitives as objects. In order to arrange graphic objects, you should use the Canvas container. The container name should be set to canvas. The required redrawing will be done automatically. For example, if you will create a new project called GraphDemo, then place a Canvas container into the window and set the event handler for Loaded event, you get the following XAML code:

<Window x:Class="GraphDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GraphDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="450">
    <Canvas Name="canvas" Loaded="Canvas_Loaded">

    </Canvas>
</Window>

Note: the main window was also slightly resized.

You can add graphical objects programmatically. The code of MainWindow.xaml.cs with Loaded event handler will be as follows:

using System;
using System.Collections.Generic;
using System.Linq; 
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace GraphDemo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Canvas_Loaded(object sender, RoutedEventArgs e)
        {
            canvas.Children.Add(new Ellipse() 
            { 
                Width = 150, 
                Height = 100, 
                Margin = new Thickness(100, 100, 0, 0),
                Fill = Brushes.Blue
            }); 
            canvas.Children.Add(new Rectangle()
            {
                Width = 150,
                Height = 100,
                Margin = new Thickness(200, 150, 0, 0),
                Fill = Brushes.Red
            }); 
        }
    }
}

Graphics primitives can be added not only in source code, but also visually or through XAML-code editor. For example, instead of creating a software code, you can add relevant items to the XAML-code to customize their properties:

<Window x:Class="GraphDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GraphDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="450">
    <Canvas Name="canvas">
        <Ellipse Canvas.Left="100" Canvas.Top="100" Height="100" Width="150" Fill="Blue" />
        <Rectangle Canvas.Left="200" Canvas.Top="150" Height="100" Width="150" Fill="Red" />
    </Canvas>
</Window>

To determine the location of shapes, so-called attached properties (Canvas.Left and Canvas.Top) are used.

The second variant does not need to implement any event. In both cases, we obtain the following result on startup:

3 Sample Programs

3.1 Working with the File System

Suppose we need to find and display information about all the files have the extension "txt" in all subdirectories starting from the specified subdirectory. Apart from the name, it is necessary to display the file size.

Bypass subdirectories is a very common task related to the file system. Therefore, it is a good idea to create universal traversal function that bypasses subdirectories and applies specific actions if necessary file was found. The event mechanism allows us to add necessary actions later.

The program will look as follows:

using System;
using System.IO;

namespace FileSystemDemo
{
    public delegate void FileEvent(FileInfo fi);
    public class FileWalker
    {
        public event FileEvent FoundEvent;

        public void WalkFiles(DirectoryInfo dir)
        {
            FileInfo[] files = dir.GetFiles();
            foreach (FileInfo f in files)
            {
                FoundEvent(f);
            }
            DirectoryInfo[] dirs = dir.GetDirectories();
            foreach (DirectoryInfo d in dirs)
            {
                WalkFiles(d);
            }
        }
    }

    class Program
    {
        // Event handler:
        static void TextFileFound(FileInfo fi)
        {
            if (fi.Extension.Equals(".txt"))
            {
                Console.WriteLine(fi.FullName + "\t" + fi.Length);
            }
        }

        static void Main(string[] args)
        {
            Console.Write("Enter folder name: ");
            string dirName = Console.ReadLine();
            DirectoryInfo dir = new DirectoryInfo(dirName);
            FileWalker walker = new FileWalker();
            walker.FoundEvent += TextFileFound;
            walker.WalkFiles(dir);
        }
    }
}

We can add several event handlers. For instance, another handler can store file information in a log file.

3.2 Using Lambda Expressions and LINQ

Suppose we want to create a static class that contains the following functions for working with lists of real numbers:

  • search for items, which squares are in a certain range
  • sort by decreasing values of sines

It is necessary to implement two approaches: by using lambda expressions and by using LINQ.

For convenience, we'll enrich the features of List<double> class. The source code of the first version would look like this:

using System;
using System.Collections.Generic;

namespace LambdaTest
{
    public static class ListsWithLambda
    {
        public static IEnumerable<double> SquaresInRange(this List<double> list, double qFrom, double qTo)
        {
            return list.FindAll(x => x * x > qFrom&& x * x <= qTo);
        }

        public static void SortBySines(this List<double> list)
        {
            // Minus provides sorting in decreasing order:
            list.Sort((d1, d2) => -Math.Sin(d1).CompareTo(Math.Sin(d2))); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<double> list = new List<double> { 1, -2, 4, 3 };
            foreach (double x in list.SquaresInRange(3, 10))
            {
                Console.Write(x + " "); // -2 3
            }
            Console.WriteLine();
            list.SortBySines();
            foreach (double x in list)
            {
                Console.WriteLine(x + "\t" + Math.Sin(x));
            }
        }
    }
}

The second version:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LINQ_Test
{
    public static class ListsWithLINQ
    {
        public static IEnumerable<double> SquaresInRange(this List<double> list, double qFrom, double qTo)
        {
            return from x in list 
                   where x * x > qFrom && x * x <= qTo 
                   select x;
        }

        public static List<double> SortBySines(this List<double> list)
        {
            return new List<double>(from x in list
                                    orderby Math.Sin(x) descending 
                                    select x); 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<double> list = new List<double> { 1, -2, 4, 3 };
            foreach (double x in list.SquaresInRange(3, 10))
            {
                Console.Write(x + " "); // -2 3
            }
            Console.WriteLine();
            list = list.SortBySines();
            foreach (double x in list)
            {
                Console.WriteLine(x + "\t" + Math.Sin(x)); 
            }
        }
    }
}

Note: The ListsWithLambda and ListsWithLINQ classes cannot be allocated in the same namespace because of name conflicts.

3.3 Working with Menus and Data Tables

Suppose we need to create a WPF application in which the user enters the size of a two-dimensional array (square matrix), types array items in calls of DataGrid component or fills them with random values. and transposes the matrix, or computes the trace o a matrix (the sum of diagonal elements) using the appropriate menu function. The sum can be displayed either using the TextBox component or in the dialog, depending on the user choice (CheckBox component).

The choice of a specific function will be implemented using RadioButton component.

We create a new WPF application named WpfMatrixApp and then change the Title of the main window (Title property) to "Working with the Square Matrix". Main grid (Grid) should be divided into four horizontal parts. This can be done by mouse clicking on the left of the grid at the required level. Level definitions appear in XAML code (RowDefinition), for example:

<Grid.RowDefinitions>
    <RowDefinition Height="22*" />
    <RowDefinition Height="40*" />
    <RowDefinition Height="246*" />
    <RowDefinition Height="43*" />
</Grid.RowDefinitions>

In order for the first, second and last parts of the grid to have a fixed height, and not adjusted to the required value, depending on the actual size of the window, we remove the asterisk after the value of Height:

<Grid.RowDefinitions>
    <RowDefinition Height="22" />
    <RowDefinition Height="40" />
    <RowDefinition Height="246*" />
    <RowDefinition Height="43" />
</Grid.RowDefinitions>

Now we add the Menu component (the future is the main menu of the application) to the top of window. We set the name of the component we set to MainMenu and also reset the Height, HorizontalAlignment, Margin, VerticalAlignment, and Width values to the default values. This can be done through the Reset Value context menu, or by removing the corresponding attributes from the XAML code. The XAML code will be as follows:

<Menu>    

In the Properties window of the MainMenu item, we can find the Items property. By selecting a button with three dots, we open the Collection Editor: Items window. Now we can add menu items (Add button) and adjust their properties. In particular, Header is the text of the menu item. We can also add a submenu via a similar Items property. In addition to menu items (MenuItem) we can add a Separator. When we close the window, we can set menu item names in the Properties window. Our main menu contains "File" submenu with "New" and "Exit" items, "Run" submenu with "Fill in with random values" and "Calculate" items , as well as the "Help" submenu with the "About" item. The representation of the menu in the XAML editor will look like this:

<Menu>
    <MenuItem Header="File">
        <MenuItem Header="New"/>
        <Separator />
        <MenuItem Header="Exit"/>
    </MenuItem>
    <MenuItem Header="Run">
        <MenuItem Header="Fill with random values"/>
        <MenuItem Header="Calculate"/>
    </MenuItem>
    <MenuItem Header="Help">
        <MenuItem Header="About..."/>
    </MenuItem>
</Menu>

The next horizontal part of the main panel contains another panel, namely gridControls. All properties in the Layout group should be set to default values. A new panel will contain the following controls: ComboBoxN of ComboBox type and two buttons of type RadioButton: RadioButtonTranspose and RadioButtonTrace with the text (Content) "Transpose" and "Find the trace" respectively. The ComboBoxN's Text property should be set to "2" and the SelectedIndex property should be set to 0. The IsChecked property of the RadioButtonTranspose control shoul be set to true.

The next horizontal part will contain a data table (DataGrid) called DataGridA. It should occupy the third part of the panel, so it also needs to reset all the properties of the Layout group.

The last part will contain a gridBottom panel with CheckBoxWindow elements of CheckBox type and TextBoxTrace of TextBox type. The last component's IsReadOnly property should be set to true. After all the settings we get the following XAML code:

<Window x:Class="WpfMatrixApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfMatrixApp"
        mc:Ignorable="d"
        Title="Working with the Square Matrix" Height="390" Width="420">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="22" />
            <RowDefinition Height="40" />
            <RowDefinition Height="246*" />
            <RowDefinition Height="43" />
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="File">
                <MenuItem Header="New"/>
                <Separator />
                <MenuItem Header="Exit"/>
            </MenuItem>
            <MenuItem Header="Run">
                <MenuItem Header="Fill with random values"/>
                <MenuItem Header="Calculate"/>
            </MenuItem>
            <MenuItem Header="Help">
                <MenuItem Header="About..."/>
            </MenuItem>
        </Menu>
        <Grid Grid.Row="1" >
            <ComboBox Height="23" Name="ComboBoxN" Margin="19,6,0,11" Width="70"
                      HorizontalAlignment="Left" Text="2" SelectedIndex="0"/>
            <RadioButton Content="Transpose" HorizontalAlignment="Left" Margin="116,11,0,0" 
                         Name="RadioButtonTranspose" IsChecked="True" />
            <RadioButton Content="Find the trace" HorizontalAlignment="Left" Margin="234,11,0,0" 
                         Name="RadioButtonTrace" />
        </Grid>
        <DataGrid AutoGenerateColumns="True" Grid.Row="2" ColumnWidth="50" 
                  CanUserResizeRows="False" Name="DataGridA"/>
        <Grid Grid.Row="3">
            <CheckBox Content="Show the result in a window" Height="16" HorizontalAlignment="Left" 
                      Margin="163,16,0,10" Name="CheckBoxWindow" VerticalAlignment="Center" />
            <TextBox Height="23" HorizontalAlignment="Left" Margin="19,10,0,0" 
                     Name="TextBoxTrace" VerticalAlignment="Top" Width="120" IsReadOnly="True" />
        </Grid>
    </Grid>
</Window>

Note: component names (Name property) it makes sense to determine when to refer to these components in the program code; in our case there are ComboBox, RadioButton, DataGrid, CheckBox and TextBox.

The implementation implies the display a square matrix items in a table. The possible approach is the use of the DataTable class of the System.Data namespace. Its DefaultView property allows you to bind the DataGrid component with some data table. In order to write data to a table cell (in row with index i and column with index j), such assignment is enough:

data.Rows[row][index] = value;

In order to read the value, conversion to string is needed:

double y = double.Parse(data.Rows[row][index] + "");

In order to work comfortably with two-dimensional arrays, we can create a wrapper class, which will work with a two-dimensional jagged array, but in fact data will be stored in the DataGrid. Such a class can be used in various programs related to the mapping of two-dimensional arrays, so it is advisable to define it in a separate class library (Class Library (.NET Core)). The DataArray.cs file of DataArrays class library will have the following content:

using System.Data;

namespace DataArrays
{
    /// <summary>
    /// Class to represent a two-dimensional array. The values of the elements
    /// are stored in System.Data.DataTable object
    /// </summary>
    public class DataArray
    {
        /// <summary>
        /// Helper class that represents a single array row
        /// </summary>
        public class DataArrayRow
        {
            private readonly DataTable data;  // reference to DataTable
            private readonly int row;         // index of the array row

            /// <summary>
            /// Helper class constructor
            /// </summary>
            /// <param name="data">reference to DataTable</param>
            /// <param name="row">index of the array row</param>
            public DataArrayRow(DataTable data, int row)
            {
                this.row = row;
                this.data = data;
            }
 
            /// <summary>
            /// Indexer to access the item
            /// </summary>
            /// <param name="index"item index</param>
            /// <returns>array item</returns>
            public double this[int index]
            {
                get => double.Parse(data.Rows[row][index] + "");
                set => data.Rows[row][index] = value;
            }
        }

        // The object in which the items are stored:
        private readonly DataTable data = new();

        /// <summary>
        /// The number of rows
        /// </summary>
        public int M { get; }

        /// <summary>
        /// The number of columns
        /// </summary>
        public int N { get; }

        /// <summary>
        /// The object in which the items are stored
        /// </summary>
        public DataTable Data
        {
            get => data;
        }

        /// <summary>
        /// Indexer to access the row
        /// </summary>
        /// <param name="index">row index</param>
        /// <returns>row object</returns>
        public DataArrayRow this[int index]
        {
            get => new(data, index);
        }

        /// <summary>
        /// The constructor in which the DataTable object is configured
        /// </summary>
        /// <param name="m">the number of rows</param>
        /// <param name="n">the number of columns</param>
        public DataArray(int m, int n)
        {
            M = m;
            N = n;
            // Add named columns (index):
            for (int j = 1; j <= N; j++)
            {
                data.Columns.Add(j + "");
            }
            // Add rows:
            for (int i = 1; i <= M; i++)
            {
                data.Rows.Add();
            }
        }
    }
}

Note: the above code requires the use of the .NET 5 platform; the appropriate settings should be made in the project properties (Project | DataArrays properties, then Target framework: .NET 5.0).

Now we are returning to the WpfMatrixApp project. We create the following event handlers:

  • For menu functions(Click event handlers):
    • for an item with the text "New": New_Click
    • for an item with the text "Exit": Exit_Click
    • for an item with the text "Fill with random values": Random_Click
    • for an item with the text "Calculate": Calc_Click
    • for an item with the text "About...": About_Click
  • For the ComboBox component SelectionChanged event handler: ComboBoxN_SelectionChanged

Note: event handler names should be entered in the Properties window next to the corresponding events.

After adding events, we get the following XAML code:

<Window x:Class="WpfMatrixApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfMatrixApp"
        mc:Ignorable="d"
        Title="Working with the Square Matrix" Height="390" Width="420">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="22" />
            <RowDefinition Height="40" />
            <RowDefinition Height="246*" />
            <RowDefinition Height="43" />
        </Grid.RowDefinitions>
        <Menu>
            <MenuItem Header="File">
                <MenuItem Header="New" Click="New_Click"/>
                <Separator />
                <MenuItem Header="Exit" Click="Exit_Click"/>
            </MenuItem>
            <MenuItem Header="Run">
                <MenuItem Header="Fill with random values" Click="Random_Click"/>
                <MenuItem Header="Calculate" Click="Calc_Click"/>
            </MenuItem>
            <MenuItem Header="Help">
                <MenuItem Header="About..." Click="About_Click"/>
            </MenuItem>
        </Menu>
        <Grid Grid.Row="1" >
            <ComboBox Height="23" Name="ComboBoxN" Margin="19,6,0,11" Width="70" SelectedIndex="0" 
                      HorizontalAlignment="Left" Text="2" SelectionChanged="ComboBoxN_SelectionChanged" />
            <RadioButton Content="Transpose" HorizontalAlignment="Left" Margin="116,11,0,0" 
                         Name="RadioButtonTranspose" IsChecked="True" />
            <RadioButton Content="Find the trace" HorizontalAlignment="Left" Margin="234,11,0,0" 
                         Name="RadioButtonTrace" />
        </Grid>
        <DataGrid AutoGenerateColumns="True" Grid.Row="2" ColumnWidth="50" 
                  CanUserResizeRows="False" Name="DataGridA"/>
        <Grid Grid.Row="3">
            <CheckBox Content="Show the result in a window" Height="16" HorizontalAlignment="Left" 
                      Margin="163,16,0,10" Name="CheckBoxWindow" VerticalAlignment="Center" />
            <TextBox Height="23" HorizontalAlignment="Left" Margin="19,10,0,0" 
                     Name="TextBoxTrace" VerticalAlignment="Top" Width="120" IsReadOnly="True" />
        </Grid>
    </Grid>
</Window>

The source code for MainWindow.xaml.cs will look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfMatrixApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void New_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Exit_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Random_Click(object sender, RoutedEventArgs e)
        {

        }

        private void Calc_Click(object sender, RoutedEventArgs e)
        {

        }

        private void About_Click(object sender, RoutedEventArgs e)
        {

        }

        private void ComboBoxN_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {

        }
    }
}

After adding the reference to DataArrays project (Add | Project Reference...), we add the InitTable() method, which creates and fills the array of specified size with zeros. Within the constructor we set up ComboBoxN and call the InitTable() function. Next, we implement event handlers.

The source code will look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using DataArrays;

namespace WpfMatrixApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DataArray a;

        public MainWindow()
        {
            InitializeComponent();
            for (int i = 2; i <= 10; i++)
            {
                ComboBoxN.Items.Add(i);
            }
            InitTable(2);
        }
        
        private void InitTable(int n)
        {
            a = new DataArray(n, n);
            DataGridA.ItemsSource = a.Data.DefaultView;
            for (int i = 0; i < a.M; i++)
            {
                for (int j = 0; j < a.N; j++)
                {
                    a[i][j] = 0;
                }
            }
            DataGridA.CanUserAddRows = false;
        }

        private void New_Click(object sender, RoutedEventArgs e)
        {
            ComboBoxN.SelectedIndex = 0;
            InitTable(2);
            RadioButtonTranspose.IsChecked = true;
            CheckBoxWindow.IsChecked = false;
            TextBoxTrace.Text = "";
        }

        private void Exit_Click(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void Random_Click(object sender, RoutedEventArgs e)
        {
            // Initialize random values generator 
            // by the number of milliseconds of the current time.
            // Without initialization, the numbers will always be the same
            Random rand = new(DateTime.Now.Millisecond);
            // Filling with random numbers from 0 to 100 with two digits 
            // after the decimal point:
            for (int i = 0; i < a.M; i++)
            {
                for (int j = 0; j < a.N; j++)
                {
                    string s =  $"{(rand.NextDouble() * 100):f2}"; // інтерполяція рядка
                    a[i][j] = Double.Parse(s);
                }
            }
        }

        private void Calc_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (RadioButtonTranspose.IsChecked ?? true)
                {
                    // transposition:
                    for (int i = 0; i < a.M; i++)
                    {
                        for (int j = i + 1; j < a.N; j++)
                        {
                            double z = a[i][j];
                            a[i][j] = a[j][i];
                            a[j][i] = z;
                        }
                    }
                }
                else
                {
                    // Calculate the trace of the matrix:
                    double trace = 0;
                    for (int i = 0; i < a.M; i++)
                    {
                        trace += a[i][i];
                    }
                    // Output the result:
                    if (CheckBoxWindow.IsChecked ?? true)
                    {
                        TextBoxTrace.Text = "";
                        MessageBox.Show($"Trace: {trace:f2}", "Result");
                    }
                    else
                    {
                        TextBoxTrace.Text = $"Trace: {trace:f2}";
                    }
                }
            }
            catch (Exception)
            {
                MessageBox.Show("Check your data!", "Error");
            }
        }

        private void About_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Matrix Calculator\n\nVersion 1.0", "About");
        }

        private void ComboBoxN_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            InitTable(int.Parse(ComboBoxN.SelectedItem + ""));
            TextBoxTrace.Text = "";
        }
    }
}

As can be seen from the example above, the modification of the elements of an array causes an automatic update. No need for any manual updates.

After the program has been started, the main window of the program will look like this:

Then we can fill the matrix, change its size, as well as perform various actions through the menu.

3.4 Creation of WPF Application for Working with Bookshelf

Assume that we need to create GUI application for processing data about books on the bookshelf. Program should provide the following functions:

  • Reading data about books from chosen XML document
  • Representation of loaded data in a table (DataGridView )
  • Representation of data about authors in a separate table
  • Editing data (modification, adding, removing)
  • Sorting books by name
  • Selecting books with particular author and representation of results in a separate window
  • Storing data in a new file.

Program should use previously created class library, but we'll use LINQ technology for working with sequences.

We add a new class called BookshelfWithLINQ to the library created in the previous training first (go to Solution Explorer, choose Add | Class... from the BookhelfLib context menu). This class will be derived from Bookshelf. We must redefine the constructor, as well as ContainsCharacters(), SortByTitle(), and SortByAuthorsCount(). In addition, for the new class, we must overload the + and - operators. The whole code will look like this:

// BookshelfWithLINQ.cs
using System.Collections.Generic;
using System.Linq;

namespace BookshelfLib
{
    public class BookshelfWithLINQ<TAuthor> : Bookshelf<TAuthor>
    {
        // Constructor
        public BookshelfWithLINQ(params Book<TAuthor>[] books) : base(books) 
        { 
        }

        public new List<Book<TAuthor>> ContainsCharacters(string characters)
        {
            var found = from book in Books 
                        where book.Title.Contains(characters)
                        select book;

            // return the search result found,
            // creating on its basis a new list of type List<Book<TAuthor>>
            // to ensure match types:
            return new List<Book<TAuthor>>(found);
        }

        // Sort by name:
        public new void SortByTitle()
        {
            Books = new List<Book<TAuthor>>(
                from book in Books
                orderby book.Title
                select book);
        }

        // Sort by count of authors:
        public new void SortByAuthorsCount()
        {
            Books = new List<Book<TAuthor>>(
                from book in Books
                orderby book.Authors.Count
                select book);
        }

        // Overloaded operator of adding book
        public static BookshelfWithLINQ<TAuthor> operator +
            (BookshelfWithLINQ<TAuthor> bookshelf, Book<TAuthor> newBook)
        {
            var newBooks = new List<Book<TAuthor>>(bookshelf.Books);
            newBooks.Add(newBook);
            return new BookshelfWithLINQ<TAuthor>() { Books = newBooks };
        }

        // Overloaded operator of removal book.
        // Using LINQ is purely illustrative
        public static BookshelfWithLINQ<TAuthor> operator -
            (BookshelfWithLINQ<TAuthor> bookshelf, Book<TAuthor> oldBook)
        {
            var newBooks = new List<Book<TAuthor>>(
                from book in bookshelf.Books
                where !book.Equals(oldBook)
                select book);
            return new BookshelfWithLINQ<TAuthor>() { Books = newBooks };
        }

    }
}

Note: the new keyword in the description of the redefined functions is not mandatory because they are not virtual, but the use of new makes the code more correct.

To test new function options, we add a new console application (BookshelfWithLINQApp) that refers to the class library:

using System;
using BookshelfLib;

namespace BookshelfWithLINQApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create an empty shelf:
            BookshelfWithLINQ<Author> bookshelf = new BookshelfWithLINQ<Author>();


            // Adding books
            bookshelf += new Book<Author>("The UML User Guide", 1999,
                                          new Author() { Name = "Grady", Surname = "Booch" },
                                          new Author() { Name = "James", Surname = "Rumbaugh" },
                                          new Author() { Name = "Ivar", Surname = "Jacobson" });
            bookshelf += new Book<Author>("Pro C# 2010 and the .NET 4 Platform", 2010,
                                          new Author() { Name = "Andrew", Surname = "Troelsen" });
            bookshelf += new Book<Author>("Thinking in Java", 2005,
                                          new Author() { Name = "Bruce", Surname = "Eckel" });

            // Display source data:
            Console.WriteLine(bookshelf);
            Console.WriteLine();

            // Looking for books with a certain sequence of characters:
            Console.WriteLine("Enter sequence of characters:");
            string sequence = Console.ReadLine();
            BookshelfWithLINQ<Author> newBookshelf =
                new BookshelfWithLINQ<Author>() { Books = bookshelf.ContainsCharacters(sequence) };


            // Output to the screen:
            Console.WriteLine("The found books:");
            Console.WriteLine(newBookshelf);
            Console.WriteLine();

            try
            {
                // Save data concerned with books:
                bookshelf.WriteBooks("Bookshelf.xml");

                // Sort by name and save in a file:
                bookshelf.SortByTitle();
                Console.WriteLine("By title:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByTitle.xml");

                // Sort by count of authors and save in a file:
                bookshelf.SortByAuthorsCount();
                Console.WriteLine("By count of authors:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
                bookshelf.WriteBooks("ByAuthorsCount.xml");

                // Create a new shelf. Use string type to store data
                TitledBookshelf<string> titledBookshelf = new TitledBookshelf<string>("Java");
                titledBookshelf += new Book<string>("Thinking in Java", 2005, "Bruce Eckel");
                Console.WriteLine("Shelf with books on Java language:");
                Console.WriteLine(titledBookshelf);
                titledBookshelf.WriteBooks("JavaBooks.xml");

                // Restore the first shelf in the original version
                bookshelf.ReadBooks("Bookshelf.xml");
                Console.WriteLine("Original state:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();

                // Remove a book about Java
                Book<Author> javaBook = bookshelf[2]; // indexer
                bookshelf -= javaBook;
                Console.WriteLine("After removal of the book:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine("-----------Exception:-----------");
                Console.WriteLine(ex.GetType());
                Console.WriteLine("------------Message:------------");
                Console.WriteLine(ex.Message);
                Console.WriteLine("----------Stack Trace:----------");
                Console.WriteLine(ex.StackTrace);
            }
        }
    }
}

The result should be the same as the previous version of the console test.

Now we can start creating the WPF application. In our case, the main window of the GUI program will contain a table that displays information about the books, buttons associated with specific functions, the area of display information about the authors.

We need to add a new project (WPF App) to the previous solution and set it as startup project. Project's name will be WpfBookshelfApp. First, we need to change the window title (Title) to the Bookshelf.

In our case, instead of Grid, it is reasonable to use the DockPanel component. This can be done in two ways:

  • remove the Grid component, add DockPanel from the Toolbox;
  • in the XAML editor, manually replace the <Grid> and </Grid> tags with <DockPanel> and </DockPanel> respectively.

A new Grid component should be added to the DockPanel. This container will be used later for allocation of buttons and text boxes. The VerticalAlignment and DockPanel.Dock properties should be set to Top, the Width and Margin properties should be reset to the default values. The Height property, for example, can be set to "54".

Note: The Grid alternative is the StackPanel container that automatically places child components sequentially in the vertical or horizontal direction.

Now we can add buttons to a grid. The buttons will be called ButtonOpen, ButtonSortByTitle, ButtonSortByAuthorsCount, ButtonSearch and ButtonSave. The Content properties of the buttons should be set accordingly in "Open", "Sort by Title", "Sort by Authors Count", "Search" and "Save". Properties can be edited both in the Properties window and in the edit box of the XAML code. In addition, we add TextBox (TextBoxSearch) between the third and fourth buttons. The HorizontalAlignment property of all these elements should be set to Left.

If we want to place multi-lines text inside the button, we remove value of the Content property and allocate Grid component inside the button. This grid will contain TextBlock component, which TextWrapping property is set to Wrap, and TextAlignment property is set to Center. For example, we get the following descriptions of ButtonSortByTitle and ButtonSortByAuthorsCount buttons:

<Button Height="42" HorizontalAlignment="Left" Margin="115,6,0,0" 
        Name="ButtonSortByTitle" VerticalAlignment="Top" Width="103">
    <Grid>
        <TextBlock Text="Sort by Title"
                   TextWrapping="Wrap" TextAlignment="Center" />
    </Grid>
</Button>
<Button Height="42" HorizontalAlignment="Left" Margin="225,6,0,0" 
        Name="ButtonSortByAuthorsCount" VerticalAlignment="Top" Width="103">
    <Grid>
        <TextBlock Text="Sort by Authors Count"
                   TextWrapping="Wrap" TextAlignment="Center" />
    </Grid>
</Button>

Now we add the DataGrid component to the bottom of the form. This table will display the authors of the books, so we'll rename it to DataGridAuthors. The value of the Width property should be reset (we can remove the relevant attributes from the XAML code). The DockPanel.Dock and VerticalAlignment properties we'll set to Bottom.

To the middle of the form we'll add a table (DataGrid) for displaying and editing data about the books (DataGridBooks). Unlike the previous table, we need to reset both for Width and Height properties.

After setting a new project as a startup and run it, we will get the following window:

Now columns should be added to the tables. This can be done by editing the Columns property. In the Collection Editor: Columns box, which we find by clicking on the "..." button, we add two new columns to the DataGridBooks table using the Add button and adjust the column properties: for the first column, the Header property of the first column should be set to "Title", and for the second column titie will be set to "Year". Similarly, we can add two columns with the names "Name" and "Surname" to the DataGridAuthors table. Column names of the DataGridBooks table should be set to ColumnTitle and ColumnYear respectively. Column names of the DataGridAuthors table should be set to ColumnName and ColumnSurname respectively. To improve the display of individual columns, it is advisable to set specific width values Width).

We can manually add the context menu by adding such XAML code after the definition of DataGridBooks columns:

<DataGrid.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Add item" Name="MenuItemAdd" />
        <MenuItem Header="Remove Item" Name="MenuItemRemove" />
    </ContextMenu>
</DataGrid.ContextMenu>

After placing and adjusting the components, we get the following XAML code:

<Window x:Class="WpfBookshelfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBookshelfApp"
        mc:Ignorable="d"
        Title="Bookshelf" Height="450" Width="685">
    <DockPanel>
        <Grid Height="54" DockPanel.Dock="Top" VerticalAlignment="Top">
            <Button Content="Open" Height="42" HorizontalAlignment="Left" Margin="5,6,0,0" 
                    Name="ButtonOpen" VerticalAlignment="Top" Width="103" />
            <Button Height="42" HorizontalAlignment="Left" Margin="115,6,0,0" 
                    Name="ButtonSortByTitle" VerticalAlignment="Top" Width="103">
                <Grid>
                    <TextBlock Text="Sort by Title" TextWrapping="Wrap" TextAlignment="Center" />
                </Grid>
            </Button>
            <Button Height="42" HorizontalAlignment="Left" Margin="225,6,0,0" 
                    Name="ButtonSortByAuthorsCount" VerticalAlignment="Top" Width="103">
                <Grid>
                    <TextBlock Text="Sort by Authors Count" TextWrapping="Wrap" 
                               TextAlignment="Center" />
                </Grid>
            </Button>
            <TextBox Height="24" HorizontalAlignment="Left" Margin="335,14,0,0" 
                    Name="TextBoxSearch" VerticalAlignment="Top" Width="109" />
            <Button Content="Search" Height="42" HorizontalAlignment="Left" Margin="450,6,0,0" 
                    Name="ButtonSearch" VerticalAlignment="Top" Width="103" />
            <Button Content="Save" Height="42" HorizontalAlignment="Left" Margin="560,6,0,0" 
                    Name="ButtonSave" VerticalAlignment="Top" Width="103" />
        </Grid>
        <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors" 
                  DockPanel.Dock="Bottom" VerticalAlignment="Bottom">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Width="100" x:Name="ColumnName" />
                <DataGridTextColumn Header="Surname" Width="100" x:Name="ColumnSurname" />
            </DataGrid.Columns>
        </DataGrid>
        <DataGrid AutoGenerateColumns="False" Name="DataGridBooks">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Title" Width="350" x:Name="ColumnTitle" />
                <DataGridTextColumn Header="Year" x:Name="ColumnYear" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Add item" Name="MenuItemAdd" />
                    <MenuItem Header="Remove Item" Name="MenuItemRemove" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </DockPanel>
</Window>

Note: the order of the attributes may be different.

Now we can start with direct coding. First, we add a reference to the BookshelfLib class library to the WpfBookshelfApp project. Then we open the MainWindow.xaml.cs file and add

using BookshelfLib;

To simplify editing it is reasonable to use class Author (instead of structure). This class can be declared in the MainWindow.xaml.cs file after the defibition of MainWindow class. The MainWindow class code now will contain a reference to BookshelfWithLINQ. We can initialize the object on the spot. The text will look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using BookshelfLib;

namespace WpfBookshelfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private BookshelfWithLINQ<Author> bookshelf = new BookshelfWithLINQ<Author>();
 
        public MainWindow()
        {
            InitializeComponent();
        }
    }
  
    public class Author
    {
        public Author() { }
  
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Surname { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Name { get; set; }

        public override bool Equals(object obj)
        {
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        public override string ToString()
        {
            return Name + " " + Surname;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

}

Now we can add a field of type BookshelfWithLINQ<Author>. To display data after the program has been started, we need to add an empty book and call the InitGrid() function within constructor's body. The corresponding code will look like this:

private BookshelfWithLINQ<Author> bookshelf = new BookshelfWithLINQ<Author>();

public MainWindow()
{
    InitializeComponent();

    // Add an empty book:
    bookshelf += new Book<Author>() 
    { 
        Title = "", 
        Authors = new List<Author> { new Author() { Name = "", Surname = "" } } 
    };
    InitGrid();
}

void InitGrid()
{
    // Bind the DataGridBooks table to the book list:
    DataGridBooks.ItemsSource = bookshelf.Books;
    DataGridBooks.CanUserAddRows = false;

    // Indicate which columns are associated with what properties:
    ColumnTitle.Binding = new Binding("Title");
    ColumnYear.Binding = new Binding("Year");

    // Show authors for the selected book:
    ShowAuthors(DataGridBooks.Items.IndexOf(DataGridBooks.SelectedItem));
}

private void ShowAuthors(int index)
{
    // If wrong index, set the index to 0
    if (index < 0 || index >= bookshelf.Books.Count)
    {
        index = 0;
    }

    // Bind the DataGridAuthors with a list of authors:
    DataGridAuthors.ItemsSource = bookshelf.Books[index].Authors;
    DataGridAuthors.CanUserAddRows = false;

    // Indicate which columns are associated with what properties:
    ColumnName.Binding = new Binding("Name");
    ColumnSurname.Binding = new Binding("Surname");
}

To display the authors of the book in the lower table, we add the SelectionChanged event handler. Its code will be as follows:

private void DataGridBooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // Display the authors of the book selected in DataGridBooks:
    ShowAuthors(DataGridBooks.Items.IndexOf(DataGridBooks.SelectedItem));
}

To add and remove the table rows, we implement event handlers:

private void MenuItemAdd_Click(object sender, RoutedEventArgs e)
{
    // Confirm changes in the table:
    DataGridBooks.CommitEdit();
    // Add an empty book:
    bookshelf += new Book<Author>()
    {
        Title = "",
        Authors = new List<Author> { new Author() { Name = "", Surname = "" } }
    };
    InitGrid();
}

private void MenuItemRemove_Click(object sender, RoutedEventArgs e)
{
    // Determine the index of the active line:
    int index = DataGridBooks.SelectedIndex;
    // Confirm changes in the table:
    DataGridBooks.CommitEdit();
    // Remove the active row:
    bookshelf.Books.RemoveAt(index);
    // If all the rows are deleted, add a new one empty:
    if (bookshelf.Books.Count == 0)
    {
        bookshelf = new BookshelfWithLINQ<Author>();
        bookshelf += new Book<Author>("", 0, new Author());
        InitGrid();
    }
    DataGridBooks.ItemsSource = null;
    InitGrid();
}

WPF does not provide components for manipulating file system. Relevant objects should be created in the code. Event handlers concerned with buttons ButtonOpen, ButtonSortByTitle, ButtonSortByAuthorsCount, and ButtonSave will look like this:

private void ButtonOpen_Click(object sender, RoutedEventArgs e)
{
    // Create a dialog windows and adjust its properties:
    Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
    dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory; // current folder
    dlg.DefaultExt = ".xml";
    dlg.Filter = "XML files (*.xml)|*.xml|XML files (*.*)|*.*";
    if (dlg.ShowDialog() == true)
    {
        try
        {
            bookshelf.ReadBooks(dlg.FileName);
        }
        catch (Exception)
        {
            MessageBox.Show("Error reading from file");
        }
        InitGrid();
    }
}

private void ButtonSave_Click(object sender, RoutedEventArgs e)
{
    // Create a dialog windows and adjust its properties:
    Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
    dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
    dlg.DefaultExt = ".xml";
    dlg.Filter = "XML files (*.xml)|*.xml|XML files (*.*)|*.*";
    if (dlg.ShowDialog() == true)
    {
        try
        {
            bookshelf.WriteBooks(dlg.FileName);
            MessageBox.Show("File saved");
        }
        catch (Exception)
        {
            MessageBox.Show("Error writing to file");
        }
    }
}

private void ButtonSortByTitle_Click(object sender, RoutedEventArgs e)
{
    bookshelf.SortByTitle();
    InitGrid();
}

private void ButtonSortByAuthorsCount_Click(object sender, RoutedEventArgs e)
{
    bookshelf.SortByAuthorsCount();
    InitGrid();
}

A separate window can be created to display search results (WindowSearchResults). Add a new window to the project (Project | Add Window...). The Title property we set to the "Search Results" value. We add a TextBox component (TextBoxSearchResults) to our grid and configure it so that it occupies the entire client part of the window. Get the following XAML code:

<Window x:Class="WpfBookshelfApp.WindowSearchResults"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBookshelfApp"
        mc:Ignorable="d"
        Title="Search Results" Height="300" Width="600">
    <Grid>
        <TextBox Name="TextBoxSearchResults" />
    </Grid>
</Window>

The ButtonSearch_Click event handler would look like this:

private void ButtonSearch_Click(object sender, RoutedEventArgs e)
{
    string sequence = TextBoxSearch.Text;
    if (sequence.Length == 0)
    {
        return;
    }
    // Find books:
    var found = bookshelf.ContainsCharacters(sequence);

    // Form a result string:
    string text = "";
    foreach (var book in found)
    {
        text += book + "\n";
    }
    // Create a new window:
    WindowSearchResults windowSearchResults = new WindowSearchResults();
    windowSearchResults.TextBoxSearchResults.Text = text;
    windowSearchResults.ShowDialog();
}

4 Exercises

  1. Create a program in which the user enters a folder name and a file name. Create a new folder and new file. Display the names of all the files in that folder. Delete the file and folder.
  2. Create a program in which the user enters a folder name and filename extension. The program displays the names of all the files in this folder with a specific extension. If the folder does not exist, it displays an error message.
  3. Create a program in which the user enters a folder name from the keyboard. Display the names of all subdirectories of this folder. If the folder does not exist, display an error message.
  4. Implement a program that calculates the definite integral using rectangles. Determine the original function using delegates.
  5. Implement a program that calculates the definite integral by trapezoids. Determine the original function using delegates.
  6. Given list of integers. Implement search of integers that begin and end with the same digit (through representation as a string). Use lambda expressions.
  7. Sort the list of integers by decreasing the last digit (using a representation in the form of a string). Use lambda expressions.
  8. Use LINQ tools to search for integers that begin and end with an identical number (using a string representation).
  9. Sort a list of integers by decreasing the last digit (using a representation in the form of a string). Use LINQ.
  10. Create a WPF application in which the user enters the circle radius and receives the image of that circle in the window.
  11. Create a WPF application in which different colors are used to draw three concentric circles.

5 Quiz

  1. What's difference between var and dynamic definitions?
  2. How to implement generic approach using dynamic typing?
  3. Can you use FileInfo class to modify the folder?
  4. Can you use FileInfo class to modify file attributes?
  5. How to programmatically create and delete a folder?
  6. What is the purpose of partial classes?
  7. How to implement partial methods?
  8. What is a callback?
  9. What is a delegate?
  10. What delegates differ from pointers to functions defined in C++?
  11. What determines the type of function?
  12. How to describe the type of delegate?
  13. How to create instances of delegates?
  14. How to create an anonymous method?
  15. What is event-driven programming?
  16. How events are implemented in C#?
  17. What are the concepts of functional programming?
  18. How to define the concept of closure?
  19. What are the benefits of lambda expressions?
  20. How to create a list of several parameters in lambda expression?
  21. What are the concepts of declarative programming?
  22. What are purposes of LINQ technology?
  23. What is the result type of LINQ query?
  24. What are advantages and disadvantages of Windows Presentation Foundation technologies?
  25. What is the usage of XAML?
  26. What are the features of the arrangement of visual elements in WPF?
  27. What are WPF containers?
  28. How to implement data binding in WPF?
  29. How to create WPF application in Visual Studio?
  30. What are the features of WPF graphics?

 

up