ua

Examples of Code Using Capabilities of C# 9 and C# 10

1 Processing Data about Books on Bookshelf (Laboratory Training 1)

The only significant addition related to the verification of potentially possible null value is recommended in the statement in which the string is entered from the keyboard. Instead of the statement

string sequence = Console.ReadLine();

the following statement should be used:

string sequence = Console.ReadLine() ?? "";

This will make it impossible to assign the null value.

2 The Hierarchy of Bookshelves (Laboratory Training 2)

Considering the features of C# 9 and C# 10, we can modify the program code in this way:

using System;

namespace LabSecond
{
    // Structure that represents an author
    public struct Author
    {
        public string Surname, Name;

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj == null)
            {
                return false;
            }
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        // Definition of string representation:
        public override string ToString()
        {
            return Name + " " + Surname;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public static bool operator ==(Author left, Author right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Author left, Author right)
        {
            return !(left == right);
        }
    }

    // Book 
    public class Book
    {
        public string Title { get; set; }
        public int Year { get; set; }
        public Author[] Authors { get; set; }

        // Constructor
        public Book(string title, int year, params Author[] authors)
        {
            Title = title;
            Year = year;
            Authors = authors;
        }

        // Definition of string representation
        // string.Format() provides a format similar to Console.WriteLine()
        public override string ToString()
        {
            string s = string.Format("Title: \"{0}\". Year of publication: {1}", Title, Year);
            s += "\n" + "   Authors:";
            for (int i = 0; i < Authors.Length; i++)
            {
                s += string.Format("      {0}", Authors[i]);
                if (i < Authors.Length - 1)
                {
                    s += ",";
                }
                else
                {
                    s += "\n";
                }
            }
            return s;
        }

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj is Book b)
            {
                if (b.Authors.Length != Authors.Length)
                {
                    return false;
                }
                for (int i = 0; i < Authors.Length; i++)
                {
                    if (!b.Authors[i].Equals(Authors[i]))
                    {
                        return false;
                    }
                }
                return b.Title == Title && b.Year == Year;
            }
            return false;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

    }

    // Bookshelf
    public class Bookshelf
    {
        public Book[] Books { get; set; }

        // Constructor
        public Bookshelf(params Book[] books)
        {
            Books = books;
        }

        // Indexer
        public Book this[int index]
        {
            get { return Books[index]; }
            set { Books[index] = value; }
        }

        // Definition of string representation
        public override string ToString()
        {
            string result = "";
            foreach (Book book in Books)
            {
                result += book;
            }
            return result;
        }

        // Looking for books with a certain sequence of characters
        public Book[] ContainsCharacters(string characters)
        {
            Book[] found = Array.Empty<Book>();
            foreach (Book book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Add a new item:
                    Array.Resize(ref found, found.Length + 1);
                    found[^1] = book;
                }
            }
            return found;
        }

        // Adding book
        public void Add(Book book)
        {
            Book[] books = Books;
            Array.Resize(ref books, Books.Length + 1);
            Books = books;
            Books[^1] = book;
        }

        // Removal of a book with the specified data
        public void Remove(Book book)
        {
            int i, k;
            Book[] newBooks = new Book[Books.Length];
            for (i = 0, k = 0; i < Books.Length; i++, k++)
            {
                if (Books[i].Equals(book))
                {
                    k--;
                }
                else
                {
                    newBooks[k] = Books[i];
                }
            }
            if (i > k)
            {
                Array.Resize(ref newBooks, Books.Length - 1);
            }
            Books = newBooks;
        }

        // Overloaded operator of adding book
        public static Bookshelf operator +(Bookshelf bookshelf, Book book)
        {
            Bookshelf newBookshelf = new(bookshelf.Books);
            newBookshelf.Add(book);
            return newBookshelf;
        }

        // Overloaded operator of removal book
        public static Bookshelf operator -(Bookshelf bookshelf, Book book)
        {
            Bookshelf newBookshelf = new(bookshelf.Books);
            newBookshelf.Remove(book);
            return newBookshelf;
        }
    }

    // Titled bookshelf
    public class TitledBookshelf : Bookshelf
    {
        public string Title { get; set; }

        public TitledBookshelf(string title, params Book[] books) : base(books)
        {
            Title = title;
        }

        // Definition of string representation
        public override string ToString()
        {
            return Title + "\n" + base.ToString();
        }

        // Overloaded operator of adding book
        public static TitledBookshelf operator +(TitledBookshelf titled, Book book)
        {
            TitledBookshelf newBookshelf = new(titled.Title, titled.Books);
            newBookshelf.Add(book);
            return newBookshelf;
        }

        // Overloaded operator of removal book
        public static TitledBookshelf operator -(TitledBookshelf titled, Book book)
        {
            TitledBookshelf newBookshelf = new(titled.Title, titled.Books);
            newBookshelf.Remove(book);
            return newBookshelf;
        }
    }

    class Program
    {
        static void Main()
        {
            // Create an empty shelf:
            Bookshelf bookshelf = new();

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

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

            // Looking for books with a certain sequence of characters:
            Console.WriteLine("Enter sequence of characters:"); 
            string sequence = Console.ReadLine() ?? "";
            Bookshelf newBookshelf = new(bookshelf.ContainsCharacters(sequence));
      
            // Output to the screen:
            Console.WriteLine("The found books:"); 
            Console.WriteLine(newBookshelf);
            Console.WriteLine();

            // Remove Java book
            Book javaBook = bookshelf[2]; // indexer
            bookshelf -= javaBook;
            Console.WriteLine("After removal of the book:"); 
            Console.WriteLine(bookshelf);
            Console.WriteLine();

            // Create a new shelf
            TitledBookshelf titledBookshelf = new TitledBookshelf("Java");
            titledBookshelf += javaBook;
            Console.WriteLine("New bookshelf:");
            Console.WriteLine(titledBookshelf);
        }
    }
}

The code uses a pattern matching mechanism, additional control of nullable types, new opportunities for creating objects (new()), overloading operations == and !=, as well as the mechanism of obtaining indices through the use of Index type.

3 Creating a Hierarchy of Generic Classes (Laboratory Training 3)

The example of this laboratory work contains a class library and a console application. The following code should be allocated in the class library:

using System.Xml.Serialization;

namespace BookshelfLib
{
    // Structure that represents an author
    public struct Author
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Surname { get; set; }

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

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj == null)
            {
                return false;
            }
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        // Definition of string representation:
        public override string ToString()
        {
            return Name + " " + Surname;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public static bool operator ==(Author left, Author right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Author left, Author right)
        {
            return !(left == right);
        }
    }

    // Book 
    public class Book<TAuthor>
    {
        [System.Xml.Serialization.XmlAttributeAttribute()]
        public string Title { get; set; }

        [System.Xml.Serialization.XmlAttributeAttribute()]
        public int Year { get; set; }

        public List<TAuthor> Authors { get; set; }

        // Constructors
        public Book()
        {
            Title = "";
            Authors = new List<TAuthor>();
        }

        public Book(string title, int year, params TAuthor[] authors)
        {
            Title = title;
            Year = year;
            Authors = new List<TAuthor>(authors);
        }

        // Definition of string representation
        override public string ToString()
        {
            string s = string.Format("Title: \"{0}\". Year of publication: {1}", Title, Year);
            s += "\n" + "   Author(s):";
            for (int i = 0; i < Authors.Count; i++)
            {
                s += string.Format("      {0}", Authors[i]);
                if (i < Authors.Count - 1)
                {
                    s += ",";
                }
                else
                {
                    s += "\n";
                }
            }
            return s;
        }

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj is Book<TAuthor> b)
            {
                if (b.Authors.Count != Authors.Count)
                {
                    return false;
                }
                for (int i = 0; i < Authors.Count; i++)
                {
                    if (!b.Authors[i].Equals(Authors[i]))
                    {
                        return false;
                    }
                }
                return b.Title == Title && b.Year == Year;
            }
            return false;
        }

        // Defined paired with Equals()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

    }

    // Bookshelf
    public class Bookshelf<TAuthor>
    {
        public List<Book<TAuthor>> Books { get; set; }

        // Constructor
        public Bookshelf(params Book<TAuthor>[] books)
        {
            Books = new List<Book<TAuthor>>(books);
        }

        // Indexer
        public Book<TAuthor> this[int index]
        {
            get { return Books[index]; }
            set { Books[index] = value; }
        }

        // Definition of string representation
        override public string ToString()
        {
            string result = "";
            foreach (Book<TAuthor> book in Books)
            {
                result += book;
            }
            return result;
        }

        // Looking for books with a certain sequence of characters
        public List<Book<TAuthor>> ContainsCharacters(string characters)
        {
            List<Book<TAuthor>> found = new();
            foreach (Book<TAuthor> book in Books)
            {
                if (book.Title.Contains(characters))
                {
                    // Adding a new element to a list:
                    found.Add(book);
                }
            }
            return found;
        }

        // Adding book
        public void Add(Book<TAuthor> book)
        {
            Books.Add(book);
        }

        // Removal of a book with the specified data
        public void Remove(Book<TAuthor> book)
        {
            Books.Remove(book);
        }

        // Reading books using deserialization mechanism
        public bool ReadBooks(string fileName)
        {
            XmlSerializer deserializer = new(typeof(List<Book<TAuthor>>));
            using TextReader textReader = new StreamReader(fileName);
            var data = deserializer.Deserialize(textReader);
            if (data == null)
            {
                return false;
            }
            Books = (List<Book<TAuthor>>)data;
            return true;
        }

        // Saving books using serialization mechanism
        public void WriteBooks(string fileName)
        {
            XmlSerializer serializer = new(typeof(List<Book<TAuthor>>));
            using TextWriter textWriter = new StreamWriter(fileName);
            serializer.Serialize(textWriter, Books);
        }

        // Nested class for comparison books in alphabetical order of names
        class CompareByTitle : IComparer<Book<TAuthor>>
        {
            public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2)
            {
                if (b1 == null || b2 == null)
                {
                    return 0;
                }
                return string.Compare(b1.Title, b2.Title);
            }
        }

        // Nested class for comparison books by the number of authors
        class CompareByAuthorsCount : IComparer<Book<TAuthor>>
        {
            public int Compare(Book<TAuthor>? b1, Book<TAuthor>? b2)
            {
                if (b1 == null || b2 == null)
                {
                    return 0;
                }
                return b1.Authors.Count < b2.Authors.Count ? -1 :
                       (b1.Authors.Count == b2.Authors.Count ? 0 : 1);
            }
        }

        // Sort by name
        public void SortByTitle()
        {
            Books.Sort(new CompareByTitle());
        }

        // Sort by count of authors
        public void SortByAuthorsCount()
        {
            Books.Sort(new CompareByAuthorsCount());
        }

        // Overloaded operator of adding book
        public static Bookshelf<TAuthor> operator +(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book)
        {
            Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books };
            newShelf.Add(book);
            return newShelf;
        }

        // Overloaded operator of removal book
        public static Bookshelf<TAuthor> operator -(Bookshelf<TAuthor> bookshelf, Book<TAuthor> book)
        {
            Bookshelf<TAuthor> newShelf = new() { Books = bookshelf.Books };
            newShelf.Remove(book);
            return newShelf;
        }
    }

    // Titled bookshelf
    public class TitledBookshelf<TAuthor> : Bookshelf<TAuthor>
    {
        public string Title { get; set; }

        // Constructor with parameters
        public TitledBookshelf(string title, params Book<TAuthor>[] books)
                  : base(books)
        {
            Title = title;
        }

        // Definition of string representation
        override public string ToString()
        {
            return Title + "\n" + base.ToString();
        }

        // Overloaded operator of adding book
        public static TitledBookshelf<TAuthor> operator +(TitledBookshelf<TAuthor> titled, Book<TAuthor> book)
        {
            TitledBookshelf<TAuthor> newShelf = new(titled.Title)
            {
                Books = titled.Books
            };
            newShelf.Add(book);
            return newShelf;
        }

        // Overloaded operator of removal book
        public static TitledBookshelf<TAuthor> operator -(TitledBookshelf<TAuthor> titled, Book<TAuthor> book)
        {
            TitledBookshelf<TAuthor> newShelf = new(titled.Title)
            {
                Books = titled.Books
            };
            newShelf.Remove(book);
            return newShelf;
        }
    }
}

The code that represents the console application will be as follows:

using BookshelfLib;

namespace BookshelfApp
{
    class Program
    {
        static void Main()
        {
            // Create an empty shelf:
            Bookshelf<Author> bookshelf = new();

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

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

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

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

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

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

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

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

                // Remove a book about Java
                Book<Author> javaBook = bookshelf[2]; // indexer
                bookshelf -= javaBook;
                Console.WriteLine("After removal of the book:");
                Console.WriteLine(bookshelf);
                Console.WriteLine();

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

            }
            catch (Exception ex)
            {
                Console.WriteLine("-----------Exception:-----------");
                Console.WriteLine(ex.GetType());
                Console.WriteLine("------------Message:------------");
                Console.WriteLine(ex.Message);
                Console.WriteLine("----------Stack Trace:----------");
                Console.WriteLine(ex.StackTrace);
            }
        }
    }
}

As in the code of previous lab, the code contains a pattern matching mechanism, additional control of nullable types, new opportunities for creating objects (new()), overloading operations == and != for value types. Also unnecessary using directives are deleted.

4 Creation of WPF Application for Working with Bookshelf (Laboratory Training 4)

The example of this laboratory training provides extension of the class library. The source code of BookshelfWithLINQ class will be like this:

// BookshelfWithLINQ.cs

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

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

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

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

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

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

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

    }
}

Appropriate console application:

using BookshelfLib;

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


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

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

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

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

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

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

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

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

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

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

The file with the XAML code of the main window remains unchanged:

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

Similarly, we leave unchanged the description of an additional window:

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

MainWindow and Author class code will be like that:

using System;
using System.Collections.Generic;
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 BookshelfWithLINQ<Author> bookshelf = new();

        public MainWindow()
        {
            InitializeComponent();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Class that represents an author
    public class Author
    {

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

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

        // Definition of the equivalence
        public override bool Equals(object? obj)
        {
            if (obj == null)
            {
                return false;
            }
            Author author = (Author)obj;
            return author.Surname == Surname && author.Name == Name;
        }

        // Definition of string representation:
        public override string ToString()
        {
            return Name + " " + Surname;
        }

        // Defined paired with Equals2()
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }
}

The code contains additional control of nullable types, new opportunities for creating objects (new()) and initialization of lists and properties. Also unnecessary using directives are deleted.

up