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 toStackPanel
, 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 an item with the text "New":
- For the
ComboBox
componentSelectionChanged
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, addDockPanel
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
- 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.
- 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.
- 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.
- Implement a program that calculates the definite integral using rectangles. Determine the original function using delegates.
- Implement a program that calculates the definite integral by trapezoids. Determine the original function using delegates.
- Use LINQ tools to search for integers that begin and end with an identical number (using a string representation).
- Sort a list of integers by decreasing the last digit (using a representation in the form of a string). Use LINQ.
- Create a WPF application in which the user enters the circle radius and receives the image of that circle in the window.
- Create a WPF application in which different colors are used to draw three concentric circles.
5 Quiz
- What is the purpose of partial classes?
- How to implement partial methods?
- Can you use
FileInfo
class to modify the folder? - Can you use
FileInfo
class to modify file attributes? - How to programmatically create and delete a folder?
- What is a callback?
- What is a delegate?
- What delegates differ from pointers to functions defined in C++?
- What determines the type of function?
- How to describe the type of delegate?
- How to create instances of delegates?
- How to create an anonymous method?
- What is event-driven programming?
- How events are implemented in C#?
- What are the concepts of functional programming?
- What are the concepts of declarative programming?
- What are purposes of LINQ technology?
- What is the result type of LINQ query?
- What are advantages and disadvantages of Windows Presentation Foundation technologies?
- What is the usage of XAML?
- What are the features of the arrangement of visual elements in WPF?
- What are WPF containers?
- How to implement data binding in WPF?
- How to create WPF application in Visual Studio?
- What are the features of WPF graphics?