Laboratory Training 5

Event-Driven Programming. Functional and Declarative Programming

1 Training Tasks

1.1 Individual Task

With the tools of Windows Presentation Foundation (WPF) subsystem, create a graphical user interface application that implements an assignment of the previous laboratory training. For searching and sorting, you should use LINQ technology. 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. Add tools for working with sequences using LINQ technology to the library.

1.2 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.3 Working with Delegates

Implement the task 1.2 ("Roots of an Equation") of the third laboratory training. Apply an approach based on the use of delegates. Complete the task using three ways of determining the left side of the equation:

  • using a separate static method;
  • using an anonymous delegate;
  • using a lambda expression.

1.4 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.5 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.

2 Instructions

2.1 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.2 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 FifthLab
{
    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.3 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 to 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.4 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.5 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.6 The Implementation of Functional and Declarative Programming in C#

2.6.1 Lambda Expressions

Functional programming in C# is implemented using 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
}

2.6.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.7 GUI Application Development using Windows Presentation Foundation Technology

2.7.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.7.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 window, or directly in the XAML code.

2.7.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.7.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.7.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.7.6 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 in the middle of the screen.

2.7.7 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 should 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}"; // string interpolation
                    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.5 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.

First, we add a new class called BookshelfProcessorWithLinq to the library created in the previous laboratory training. (go to Solution Explorer , select Add | Class... in the BookshelfLib context menu). This class will be static. It is necessary to propose an alternative implementation for the search and sorting methods that were previously implemented in the BookshelfProcessor clas . All class code will look like this:

// BookshelfProcessorWithLinq.cs
namespace BookshelfLib
{
    /// <summary>
    /// Provides methods for finding and sorting publications on a shelf
    /// </summary>
    public static class BookshelfProcessorWithLinq
    {

        /// <summary>
        /// Searches for a specified sequence of characters in publication titles
        /// </summary>
        /// <param name="bookshelf">bookshelf</param>
        /// <param name="characters">character sequence to find</param>
        /// <returns>an array of publications whose titles contain the specified sequence</returns>
        public static List<Publication> ContainsCharacters(Bookshelf bookshelf, string characters)
        {
            var found = from publication in bookshelf.Publications
                        where publication.Title.Contains(characters)
                        select publication;
            return found.ToList();
        }

        /// <summary>
        /// Sorts publications alphabetically by name ignoring case
        /// </summary>
        /// <param name="bookshelf">bookshelf</param>
        public static void SortByTitles(Bookshelf bookshelf)
        {
            var sorted = from publication in bookshelf.Publications
                         orderby publication.Title.ToUpper()
                         select publication;
            bookshelf.Publications = sorted.ToList();
        }
    }
}

To test new versions of methods, we add a new class to the console application:

// LinqTesting.cs
using BookshelfLib;

namespace BookshelfApp
{
    public static class LinqTesting
    {
        /// <summary>
        /// Demonstrates how to search and sort publications
        /// </summary>
        /// <param name="bookshelf">the bookshelf for which the work is demonstrated</param>
        public static void HandleBookshelf(Bookshelf bookshelf)
        {
            Console.WriteLine("\nInitial state:");
            Console.WriteLine(bookshelf);
            Console.WriteLine("\nTitles that contain \"The\"");
            var result = BookshelfProcessorWithLinq.ContainsCharacters(bookshelf, "The"); // class name changed
            foreach (var publication in result)
            {
                Console.WriteLine(publication.Title);
            }
            Console.WriteLine("\nAlphabetically ignoring case:");
            BookshelfProcessorWithLinq.SortByTitles(bookshelf);                           // class name changed
            Console.WriteLine(bookshelf);
        }
    }
}

The corresponding change should be made in the code of the Main() method:

/// <summary>
/// The starting point of the console application
/// </summary>
static void Main()
{
    Console.OutputEncoding = System.Text.Encoding.UTF8;
    Bookshelf bookshelf = CreateBookshelf();
    LinqTesting.HandleBookshelf(bookshelf); // use LINQ
    FileUtils.WriteToFile(bookshelf, "publications.txt");
    AdditionalProcessing("books.txt"); // No such file
    AdditionalProcessing("publications.txt");
    XMLProcessing(bookshelf, "publications.xml");
}

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

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 publications, buttons associated with specific functions, the area of display information about the authors (for books only).

We need to add a new project (WPF Application) 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, ButtonSearch and ButtonSave. The Content properties of the buttons should be set accordingly in "Open", "Sort by Title", "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 second and the third buttons. The HorizontalAlignment property of all these elements should be set to Left.

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 publications (DataGridPublications).Unlike the previous table, we need to reset both for Width and Height properties.

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 four new columns to the DataGridPublications table using the Add button and adjust the column properties: for the first column, the Header property of the columns should be set to "Title", "Year", "Volume" and "Number" accordingly. Similarly, we can add two columns with the names "Author Name" and "Author Surname" to the DataGridAuthors table. Column names of the DataGridPublications table should be set to ColumnTitle , ColumnYear, ColumnVolume and ColumnNumber 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 DataGridPublications columns:

<DataGrid.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Add Book" />
        <MenuItem Header="Add Magazine"  />
        <MenuItem Header="Remove Publication" />
    </ContextMenu>
</DataGrid.ContextMenu>

Similarly, we add a context menu with the items "Add Author" and "Remove Author" to the DataGridAuthors component.

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"
        mc:Ignorable="d"
        Title="Bookshelf" Height="450" Width="720">
    <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="135" />
            <Button Content="Sort by Title" Height="42" HorizontalAlignment="Left" Margin="145,6,0,0" 
                    Name="ButtonSortByTitle" VerticalAlignment="Top" Width="135" >
            </Button>                
            <TextBox Height="24" HorizontalAlignment="Left" Margin="285,14,0,0" 
                     Name="TextBoxSearch" VerticalAlignment="Top" Width="135" />
            <Button Content="Search" Height="42" HorizontalAlignment="Left" Margin="425,6,0,0" 
                    Name="ButtonSearch" VerticalAlignment="Top" Width="135" />
            <Button Content="Save" Height="42" HorizontalAlignment="Left" Margin="565,6,0,0" 
                    Name="ButtonSave" VerticalAlignment="Top" Width="135" />
        </Grid>
        <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors"
                  DockPanel.Dock="Bottom" VerticalAlignment="Bottom">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Author Name" Width="120" x:Name="ColumnName" />
                <DataGridTextColumn Header="Author Surname" Width="120" x:Name="ColumnSurname" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu x:Name="ContextMenuAuthors">
                    <MenuItem Header="Add Author" />
                    <MenuItem Header="Remove Author" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
        <DataGrid AutoGenerateColumns="False" x:Name="DataGridPublications" >
            <DataGrid.Columns>
                <DataGridTextColumn Header="Title" Width="350" x:Name="ColumnTitle" />
                <DataGridTextColumn Header="Publication Year" Width="100" x:Name="ColumnYear" />
                <DataGridTextColumn Header="Volume" Width="50" x:Name="ColumnVolume" />
                <DataGridTextColumn Header="Number" Width="50" x:Name="ColumnNumber" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Add Book" />
                    <MenuItem Header="Add Magazine"  />
                    <MenuItem Header="Remove Publication" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </DockPanel>
</Window>

Note: the order of the attributes may be different.

If we set a new project as the startup project and start it, we'll get the following window:

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;

Now we can add a field of type Bookshelf. We can initialize the object at the same time. To display data after the program has been started, we call the InitGrid() function within constructor's body. The corresponding code will look like this:

private Bookshelf bookshelf = new();
public MainWindow()
{ 
    InitializeComponent();
    DataGridAuthors.ContextMenu = null;
    InitGrid();
}

void InitGrid()
{
    DataGridPublications.ItemsSource = null;            
    // Bind DataGridPublications to the list of publications:
    DataGridPublications.ItemsSource = bookshelf.Publications;
    DataGridPublications.CanUserAddRows = false;
    // Specify which columns are bound to which properties:
    ColumnTitle.Binding = new Binding("Title");
    ColumnYear.Binding = new Binding("Year");
    ColumnVolume.Binding = new Binding("Volume")
    ColumnNumber.Binding = new Binding("Number")
    // Show authors for the selected book:
    ShowAuthors();
}

private void ShowAuthors()
{
    if (bookshelf.Publications.Count <= 0)
    {
        DataGridAuthors.ItemsSource = null;
        return;
    }
    int index = DataGridPublications.Items.IndexOf(DataGri
    // If the index is invalid, set the index to 0
    if (index < 0 || index >= bookshelf.Publications
    {
        index = 0;
    }
    DataGridAuthors.ItemsSource = null;
    // Bind DataGridAuthors to the list of authors:
    if (bookshelf.Publications[index] is Book book)
    {
        ColumnVolume.IsReadOnly = true;
        ColumnNumber.IsReadOnly = true;
        DataGridAuthors.ItemsSource = null;
        DataGridAuthors.ItemsSource = book.Authors;
        DataGridAuthors.CanUserAddRows = false;
        // Specify which columns are bound to which properties:
        ColumnName.Binding = new Binding("Name")
        ColumnSurname.Binding = new Binding("Surname&
        DataGridAuthors.ContextMenu = ContextMenuAuthors;
    }
    if (bookshelf.Publications[index] is Magazine)
    {
        ColumnVolume.IsReadOnly = false;
        ColumnNumber.IsReadOnly = false;
        DataGridAuthors.ItemsSource = null;
        DataGridAuthors.ContextMenu = null;
    }
}

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 DataGridPublications_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // Display the authors of the book selected in DataGridPublications:
    ShowAuthors(DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem));
}

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

private void MenuItemAddBook_Click(object sender, RoutedEventArgs e)
{
    // Confirm changes in the table:
    DataGridPublications.CommitEdit();
    // Add an empty book:
    bookshelf.AddPublication(new Book("", 0));
    InitGrid();
}

private void MenuItemAddMagazine_Click(object sender, RoutedEventArgs e)
{
    // Confirm changes in the table:
    DataGridPublications.CommitEdit();
    // Add an empty magazine:
    bookshelf.AddPublication(new Magazine()
    {
        Title = "",
        Year = 0,
        Volume = 0,
        Number = 0
    });
    InitGrid();
}


private void MenuItemRemove_Click(object sender, RoutedEventArgs e)
{
    // Determine the index of the active row:
    int index = DataGridPublications.SelectedIndex;
    // Confirm changes in the table:
    DataGridPublications.CommitEdit();
    // Remove the active row:
    bookshelf.Publications.RemoveAt(index);
    DataGridPublications.ItemsSource = null;
    InitGrid();
    if (bookshelf.Publications.Count == 0)
    {
        DataGridAuthors.ContextMenu = null;
    }
}

private void MenuItemAddAuthor_Click(object sender, RoutedEventArgs e)
{
    int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem);
    // If the index is invalid, set the index to 0
    if (index < 0 || index >= bookshelf.Publications.Count)
    {
        index = 0;
    }
    if (bookshelf.Publications[index] is Book book)
    {
        // Confirm changes in the table:
        DataGridAuthors.CommitEdit();
        book.AddAuthor("", "");
        ShowAuthors();
    }
}

private void MenuItemRemoveAuthor_Click(object sender, RoutedEventArgs e)
{
    int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem);
    // If the index is invalid, set the index to 0
    if (index < 0 || index >= bookshelf.Publications.Count)
    {
        index = 0;
    }
    if (bookshelf.Publications[index] is Book book)
    {
        // Determine the index of the active row:
        int authorIndex = DataGridAuthors.SelectedIndex;
        // Confirm changes in the table:
        DataGridAuthors.CommitEdit();
        // Remove the active row:
        book.Authors.RemoveAt(authorIndex);
        DataGridAuthors.ItemsSource = null;
        ShowAuthors();
    }
}

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

private void ButtonOpen_Click(object sender, RoutedEventArgs e)
{
    // Create an open file dialog and set its properties:
    Microsoft.Win32.OpenFileDialog dlg = new();
    dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory; // current directory
    dlg.DefaultExt = ".xml";
    dlg.Filter = "XML Files (*.xml)|*.xml|All Files (*.*)|*.*";
    if (dlg.ShowDialog() == true)
    {
        try
        {
            bookshelf = XMLHandle.ReadFromFile(dlg.FileName);
        }
        catch (Exception)
        {
            MessageBox.Show("Error reading from file");
        }
        InitGrid();
    }
}

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

The event handler for the button ButtonSortByTitle will be as follows:

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

A separate window can be created to display search results (WindowSearchResults). We 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 will look like this:

private void ButtonSearch_Click(object sender, RoutedEventArgs e)
{
    string sequence = TextBoxSearch.Text;
    if (sequence.Length == 0)
    {
        return;
    }
    // Find books by criterion:
    var found = BookshelfProcessor.ContainsCharacters(bookshelf, sequence);
    // Create the result string:
    StringBuilder text = new("");
    if (found.Count > 0)
    {
        foreach (var publication in found)
        {
            text.Append(publication.Title + "\n");
        }
    }
    else
    {
        text.Append("Nothing found");
    }
    // Create a new window:
    WindowSearchResults windowSearchResults = new();
    windowSearchResults.TextBoxSearchResults.Text = text.ToString();
    windowSearchResults.ShowDialog();
}

After adding the event handlers, the file MainWindow.xaml will look like this:

<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"
        mc:Ignorable="d"
        Title="Bookshelf" Height="450" Width="720">
    <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="135" Click="ButtonOpen_Click" />
            <Button Content="Sort by Title" Height="42" HorizontalAlignment="Left" Margin="145,6,0,0" 
                    Name="ButtonSortByTitle" VerticalAlignment="Top" Width="135" Click="ButtonSortByTitle_Click">
            </Button>
            <TextBox Height="24" HorizontalAlignment="Left" Margin="285,14,0,0" 
                     Name="TextBoxSearch" VerticalAlignment="Top" Width="135" />
            <Button Content="Search" Height="42" HorizontalAlignment="Left" Margin="425,6,0,0" 
                    Name="ButtonSearch" VerticalAlignment="Top" Width="135" Click="ButtonSearch_Click" />
            <Button Content="Save" Height="42" HorizontalAlignment="Left" Margin="565,6,0,0" 
                    Name="ButtonSave" VerticalAlignment="Top" Width="135" Click="ButtonSave_Click" />
        </Grid>
        <DataGrid AutoGenerateColumns="False" Height="130" Name="DataGridAuthors"
                  DockPanel.Dock="Bottom" VerticalAlignment="Bottom">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Author Name" Width="120" x:Name="ColumnName" />
                <DataGridTextColumn Header="Author Surname" Width="120" x:Name="ColumnSurname" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu x:Name="ContextMenuAuthors">
                    <MenuItem Header="Add Author" Click="MenuItemAddAuthor_Click" />
                    <MenuItem Header="Remove Author" Click="MenuItemRemoveAuthor_Click" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
        <DataGrid AutoGenerateColumns="False" x:Name="DataGridPublications" 
                  SelectionChanged="DataGridPublications_SelectionChanged">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Title" Width="350" x:Name="ColumnTitle" />
                <DataGridTextColumn Header="Publication Year" Width="100" x:Name="ColumnYear" />
                <DataGridTextColumn Header="Volume" Width="50" x:Name="ColumnVolume" />
                <DataGridTextColumn Header="Number" Width="50" x:Name="ColumnNumber" />
            </DataGrid.Columns>
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Add Book" Click="MenuItemAddBook_Click" />
                    <MenuItem Header="Add Magazine" Click="MenuItemAddMagazine_Click"  />
                    <MenuItem Header="Remove Publication" Click="MenuItemRemove_Click" />
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </DockPanel>
</Window>

The final MainWindow class code will look like this:

using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using BookshelfLib;

namespace WpfBookshelfApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private Bookshelf bookshelf = new();

        public MainWindow()
        { 
            InitializeComponent();
            DataGridAuthors.ContextMenu = null;
            InitGrid();
        }

        void InitGrid()
        {
            DataGridPublications.ItemsSource = null;            
            // Bind DataGridPublications to the list of publications:
            DataGridPublications.ItemsSource = bookshelf.Publications;
            DataGridPublications.CanUserAddRows = false;

            // Specify which columns are bound to which properties:
            ColumnTitle.Binding = new Binding("Title");
            ColumnYear.Binding = new Binding("Year");
            ColumnVolume.Binding = new Binding("Volume");
            ColumnNumber.Binding = new Binding("Number");

            // Show authors for the selected book:
            ShowAuthors();
        }

        private void ShowAuthors()
        {
            if (bookshelf.Publications.Count <= 0)
            {
                DataGridAuthors.ItemsSource = null;
                return;
            }
            int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem);
            // If the index is invalid, set the index to 0
            if (index < 0 || index >= bookshelf.Publications.Count)
            {
                index = 0;
            }
            DataGridAuthors.ItemsSource = null;
            // Bind DataGridAuthors to the list of authors:
            if (bookshelf.Publications[index] is Book book)
            {
                ColumnVolume.IsReadOnly = true;
                ColumnNumber.IsReadOnly = true;
                DataGridAuthors.ItemsSource = null;
                DataGridAuthors.ItemsSource = book.Authors;
                DataGridAuthors.CanUserAddRows = false;
                // Specify which columns are bound to which properties:
                ColumnName.Binding = new Binding("Name");
                ColumnSurname.Binding = new Binding("Surname");
                DataGridAuthors.ContextMenu = ContextMenuAuthors;

            }
            if (bookshelf.Publications[index] is Magazine)
            {
                ColumnVolume.IsReadOnly = false;
                ColumnNumber.IsReadOnly = false;
                DataGridAuthors.ItemsSource = null;
                DataGridAuthors.ContextMenu = null;
            }
        }

        private void DataGridPublications_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Display the authors of the book selected in DataGridPublications:
            ShowAuthors();
        }

        private void MenuItemAddBook_Click(object sender, RoutedEventArgs e)
        {
            // Confirm changes in the table:
            DataGridPublications.CommitEdit();
            // Add an empty book:
            bookshelf.AddPublication(new Book("", 0));
            InitGrid();
        }

        private void MenuItemAddMagazine_Click(object sender, RoutedEventArgs e)
        {
            // Confirm changes in the table:
            DataGridPublications.CommitEdit();
            // Add an empty magazine:
            bookshelf.AddPublication(new Magazine()
            {
                Title = "",
                Year = 0,
                Volume = 0,
                Number = 0
            });
            InitGrid();
        }

        private void MenuItemRemove_Click(object sender, RoutedEventArgs e)
        {
            // Determine the index of the active row:
            int index = DataGridPublications.SelectedIndex;
            // Confirm changes in the table:
            DataGridPublications.CommitEdit();
            // Remove the active row:
            bookshelf.Publications.RemoveAt(index);
            DataGridPublications.ItemsSource = null;
            InitGrid();
            if (bookshelf.Publications.Count == 0)
            {
                DataGridAuthors.ContextMenu = null;
            }
        }

        private void MenuItemAddAuthor_Click(object sender, RoutedEventArgs e)
        {
            int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem);
            // If the index is invalid, set the index to 0
            if (index < 0 || index >= bookshelf.Publications.Count)
            {
                index = 0;
            }
            if (bookshelf.Publications[index] is Book book)
            {
                // Confirm changes in the table:
                DataGridAuthors.CommitEdit();
                book.AddAuthor("", "");
                ShowAuthors();
            }
        }

        private void MenuItemRemoveAuthor_Click(object sender, RoutedEventArgs e)
        {
            int index = DataGridPublications.Items.IndexOf(DataGridPublications.SelectedItem);
            // If the index is invalid, set the index to 0
            if (index < 0 || index >= bookshelf.Publications.Count)
            {
                index = 0;
            }
            if (bookshelf.Publications[index] is Book book)
            {
                // Determine the index of the active row:
                int authorIndex = DataGridAuthors.SelectedIndex;
                // Confirm changes in the table:
                DataGridAuthors.CommitEdit();
                // Remove the active row:
                book.Authors.RemoveAt(authorIndex);
                DataGridAuthors.ItemsSource = null;
                ShowAuthors();
            }
        }

        private void ButtonOpen_Click(object sender, RoutedEventArgs e)
        {
            // Create an open file dialog and set its properties:
            Microsoft.Win32.OpenFileDialog dlg = new();
            dlg.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory; // current directory
            dlg.DefaultExt = ".xml";
            dlg.Filter = "XML Files (*.xml)|*.xml|All Files (*.*)|*.*";
            if (dlg.ShowDialog() == true)
            {
                try
                {
                    bookshelf = XMLHandle.ReadFromFile(dlg.FileName);
                }
                catch (Exception)
                {
                    MessageBox.Show("Error reading from file");
                }
                InitGrid();
            }
        }

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

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

        private void ButtonSearch_Click(object sender, RoutedEventArgs e)
        {
            string sequence = TextBoxSearch.Text;
            if (sequence.Length == 0)
            {
                return;
            }
            // Find books by criterion:
            var found = BookshelfProcessor.ContainsCharacters(bookshelf, sequence);
            // Create the result string:
            StringBuilder text = new("");
            if (found.Count > 0)
            {
                foreach (var publication in found)
                {
                    text.Append(publication.Title + "\n");
                }
            }
            else
            {
                text.Append("Nothing found");
            }
            // Create a new window:
            WindowSearchResults windowSearchResults = new();
            windowSearchResults.TextBoxSearchResults.Text = text.ToString();
            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. Use LINQ tools to search for integers that begin and end with an identical number (using a string representation).
  7. Sort a list of integers by decreasing the last digit (using a representation in the form of a string). Use LINQ.
  8. Create a WPF application in which the user enters the circle radius and receives the image of that circle in the window.
  9. Create a WPF application in which different colors are used to draw three concentric circles.

5 Quiz

  1. What is the purpose of partial classes?
  2. How to implement partial methods?
  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 a callback?
  7. What is a delegate?
  8. What delegates differ from pointers to functions defined in C++?
  9. What determines the type of function?
  10. How to describe the type of delegate?
  11. How to create instances of delegates?
  12. How to create an anonymous method?
  13. What is event-driven programming?
  14. How events are implemented in C#?
  15. What are the concepts of functional programming?
  16. What are the concepts of declarative programming?
  17. What are purposes of LINQ technology?
  18. What is the result type of LINQ query?
  19. What are advantages and disadvantages of Windows Presentation Foundation technologies?
  20. What is the usage of XAML?
  21. What are the features of the arrangement of visual elements in WPF?
  22. What are WPF containers?
  23. How to implement data binding in WPF?
  24. How to create WPF application in Visual Studio?
  25. What are the features of WPF graphics?

 

up