Book Image

C# 8 and .NET Core 3 Projects Using Azure - Second Edition

By : Paul Michaels, Dirk Strauss, Jas Rademeyer
Book Image

C# 8 and .NET Core 3 Projects Using Azure - Second Edition

By: Paul Michaels, Dirk Strauss, Jas Rademeyer

Overview of this book

.NET Core is a general-purpose, modular, cross-platform, and opensource implementation of .NET. The latest release of .NET Core 3 comes with improved performance and security features, along with support for desktop applications. .NET Core 3 is not only useful for new developers looking to start learning the framework, but also for legacy developers interested in migrating their apps. Updated with the latest features and enhancements, this updated second edition is a step-by-step, project-based guide. The book starts with a brief introduction to the key features of C# 8 and .NET Core 3. You'll learn to work with relational data using Entity Framework Core 3, before understanding how to use ASP.NET Core. As you progress, you’ll discover how you can use .NET Core to create cross-platform applications. Later, the book will show you how to upgrade your old WinForms apps to .NET Core 3. The concluding chapters will then help you use SignalR effectively to add real-time functionality to your applications, before demonstrating how to implement MongoDB in your apps. Finally, you'll delve into serverless computing and how to build microservices using Docker and Kubernetes. By the end of this book, you'll be proficient in developing applications using .NET Core 3.
Table of Contents (13 chapters)

Exploring XAML Islands

For this section, you will need to be running Windows 10 1903 or later. By the time this book is published, it is expected that the 1903 release will be delivered automatically to all Windows 10 machines; however, if you are running an earlier version, then you can force an update by visiting the following link: https://www.microsoft.com/en-us/software-download/windows10.

In 2019, when this chapter was written, we noted that the TreeView in the import books section looks a little dated. In fact, you'd think it was a TreeView from 2005 when WinForms was all the rage! Also, we'd like to bind our data to the TreeView, rather than build it up separately. While there are some data binding capabilities in WinForms, we are stuck with the general appearance of TreeView.

Unless, that is, we use one of the nice new UWP controls in WinForms. That's exactly what XAML Islands gives us! We can take an existing UWP control, or even create our own, and use it directly from an existing WinForms application.

Let's try and use the TreeView from the UWP Community Toolkit inside our WinForms application.

UWP TreeView

There are a number of setup requirements for this, which I'll detail later.

By the time this is published, the process for setting this up may have been simplified considerably; please refer to the linked articles for the most recent advice.

The first step is to ensure (as detailed in the Technical requirements section) that you're running Windows 10, version 1903 or later. Please follow the information in that section if you are not. The second step is to install the Windows 10 SDK; for this, you can use the following link: https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk.

We will be performing the following article for the next step: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance#set-up-your-project.

Add the following NuGet package to your WinForms project:

Microsoft.Windows.SDK.Contracts

Install the XamlHost NuGet package into the WinForms app:

Install-Package Microsoft.Toolkit.Forms.UI.XamlHost

Now we can replace our existing TreeView with the UWP one.

You'll notice that I've fully qualified all the XAML controls. Since we're dealing with two disparate frameworks, this kind of change makes it very easy to get confused and mix up which control you're dealing with.
In the following code samples, I've included class-level variables with the code samples for clarity. I, personally, would suggest that these actually go at the top of your class file. Of course, it makes no functional difference.

The first thing that we need to consider is XamlHost.

WIndowsXamlHost

Let's create our TreeView; we'll do this in the code-behind for ImportBooks.cs. We're going to add some code to the constructor, which will now look as follows:

private readonly Microsoft.Toolkit.Forms.UI.XamlHost.WindowsXamlHost _windowsXamlHostTreeView;        

public ImportBooks() { InitializeComponent(); _jsonPath = Path.Combine(Application.StartupPath, "bookData.txt"); spaces = spaces.ReadFromDataStore(_jsonPath); var windowsXamlHostTreeView = new WindowsXamlHost(); windowsXamlHostTreeView.InitialTypeName = "Windows.UI.Xaml.Controls.TreeView"; windowsXamlHostTreeView.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; windowsXamlHostTreeView.Location = new System.Drawing.Point(12, 60); windowsXamlHostTreeView.Name = "tvFoundBooks"; windowsXamlHostTreeView.Size = new System.Drawing.Size(513, 350); windowsXamlHostTreeView.TabIndex = 8; windowsXamlHostTreeView.Dock = System.Windows.Forms.DockStyle.None; windowsXamlHostTreeView.ChildChanged += windowsXamlHostTreeView_ChildChanged; this.Controls.Add(windowsXamlHostTreeView); }

Let's quickly review what we've done here (it's actually not that much). Firstly, we've created a new WIndowsXamlHost object. This is the basis for XAML Islands; it acts as a wrapper around your UWP control, so it will work in a WinForms context.


Although this chapter discusses WinForms, the same is true for WPF and, while the exact syntax may differ slightly, the basic principle is the same.

The things to notice on this code sample are as follows:

  • We're setting the name to tvFoundBooks, which is the same name as our WinForms app had.
  • We're listening to the ChildChanged event: this is so that we can set some specifics on the control itself (we'll come back to this shortly).
  • The InitialTypeName is how XAML Islands knows which UWP control to invoke.
  • We're adding the host control to the current form (we also set the location).

ItemTemplate

Now that we've set up the host control, we can have a look at the ChildChanged event that we mentioned; this is where we set up the UWP control (rather than the host control):

private Windows.UI.Xaml.Controls.TreeView? _tvFoundBooks = null;

private
void windowsXamlHostTreeView_ChildChanged(object? sender, EventArgs e) { if (sender == null) return; var host = (WindowsXamlHost)sender; _tvFoundBooks = (Windows.UI.Xaml.Controls.TreeView)host.Child; _tvFoundBooks.ItemInvoked += _tvFoundBooks_ItemInvoked; _tvFoundBooks.ItemsSource = DataSource; const string Xaml = "<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"><TreeViewItem ItemsSource=\"{Binding Children}\" Content=\"{Binding Name}\"/></DataTemplate>"; var xaml = XamlReader.Load(Xaml); _tvFoundBooks.ItemTemplate = xaml as Windows.UI.Xaml.DataTemplate; }

Don't worry so much about why _tvFoundBooks is a class-level variable, we'll come back to that shortly. In the preceding code sample, we have a gated check to ensure that sender is not null, and then we're forcing it to a WindowsXamlHost type. Once we have this type, we can get whatever is inside the host by calling the .Child property.

As before, we're listening to the ItemInvoked event (again, we'll come back to this shortly). The first really new thing here is that we're setting the ItemsSource, and the ItemTemplate. We'll come back to ItemsSource, but the template is worth exploring. Unlike WinForms, UWP uses XAML to define how its controls look. This means that you have control over exactly what goes into the TreeView; for example, each node could have an image, or text, or both. However, if you don't specify ItemTemplate, then the rendering engine doesn't know what to display, or how.

The preceding XAML is probably the simplest one that will display anything. You'll notice there are a few binding statements; they are binding to properties relative to the ItemsSource. Let's have a look at exactly what it is we're binding to.

TreeView Item model and ItemsSource

In order to bind something to a control in UWP, you need something. Essentially, what that means is that we need a model.


A model, in .NET terms, is simply a class that holds data.

We're going to create a new class, and we'll call it Item:

public class Item
{
    public string Name { get; set; }
    public ObservableCollection<Item> Children { get; set; } = new ObservableCollection<Item>();
    public ItemType ItemType { get; set; }
    public string FullName { get; set; }
 
    public override string ToString()
    {
        return Name;
    }
}
I would always recommend that models are held in their own file and sit in a folder called Models, but there's no technical reason why you couldn't add this class to the end of ImportBooks.cs.

Most of this class should be self-explanatory; we're holding the Name and FullName (that is, the name and path) of the file. The ObservableCollection is a special type of Collection that allows the UI framework to be notified when it changes.


For the code that we're writing here, we could get away with this simply being a List; however, ObservableCollection is good practice when dealing with desktop XAML frameworks such as UWP, and this will make extensibility easier.

Finally, we're holding the type of the item, which is a new enumerated type:

public enum ItemType
{
    Docx,
    Docxx,
    Pdfx,
    Epubx,
    Folder
}

Back in ImportBooks.cs, we're going to set up our ItemsSource. The first step is to add a class-level variable called DataSource:

public ObservableCollection<Models.Item> DataSource { get; set; }

Our next change is in the btnSelectSourceFolder_Click event handler:

private void btnSelectSourceFolder_Click(object sender, EventArgs e)
{
    try
    {
        FolderBrowserDialog fbd = new FolderBrowserDialog();
        fbd.Description = "Select the location of your eBooks and documents";
 
        DialogResult dlgResult = fbd.ShowDialog();
        if (dlgResult == DialogResult.OK)
        {
            UpdateBookList(fbd.SelectedPath);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

As you can see, the new method is hugely simplified compared to the previous version; we've extracted all the real logic into a new method, so let's see that next:

private void UpdateBookList(string path)
{            
    DirectoryInfo di = new DirectoryInfo(path);
    var bookList = new List<Models.Item>();
    var rootItem = new Models.Item()
    {
        Name = di.Name
    };
 
    rootItem.ItemType = Models.ItemType.Folder;
 
    PopulateBookList(di.FullName, rootItem);
    bookList.Add(rootItem);
 
    DataSource = new ObservableCollection<Models.Item>(bookList);
    _tvFoundBooks.ItemsSource = DataSource.OrderBy(a => a.Name);
}

Here, we're setting up the root item of our TreeView; however, you'll notice that the only reference that we actually have to the TreeView is at the end, where we refresh ItemsSource. PopulateBookList is our next port of call. As before, this method is essentially in two parts; let's see the first part:

public void PopulateBookList(string paramDir, Models.Item rootItem)
{
    if (rootItem == null) throw new ArgumentNullException();

rootItem.FullName = paramDir;
rootItem.ItemType = Models.ItemType.Folder; DirectoryInfo dir = new DirectoryInfo(paramDir); foreach (DirectoryInfo dirInfo in dir.GetDirectories()) { var item = new Models.Item(); item.Name = dirInfo.Name; rootItem.Children.Add(item); PopulateBookList(dirInfo.FullName, item); }

Here, we're recursively traversing the directory structure and populating our new model. Notice that we're setting the item type and the FullName (the directory path) at the start, and then we iterate through all the sub-directories, re-calling our method.

Recursion is the practice of calling a method from itself. Is can be very useful in scenarios such as this, where you wish to perform exactly the same operation on nested objects. It is faster than using a loop; however, it does have the potential to fill up the stack very quickly if used incorrectly.

For the second part of the function, we'll process any files that are in the current directory (that is, whichever directory is at the top of the recursion stack at the time):

    foreach (FileInfo fleInfo in dir.GetFiles().Where(x => AllowedExtensions.Contains(x.Extension)).ToList())
    {
        var item = new Models.Item();
        item.Name = fleInfo.Name;
 
        item.FullName = fleInfo.FullName;
        item.ItemType = (Models.ItemType)Enum.Parse(typeof(Extention), fleInfo.Extension.TrimStart('.'), true);
 
        rootItem.Children.Add(item);
    }
}

Our next change is to the ItemInvoked method; the new method should look as follows:

private void _tvFoundBooks_ItemInvoked(Windows.UI.Xaml.Controls.TreeView sender, Windows.UI.Xaml.Controls.TreeViewItemInvokedEventArgs args)
{
    var selectedItem = (Models.Item)args.InvokedItem;
 
    DocumentEngine engine = new DocumentEngine();
    string path = selectedItem.FullName.ToString();
 
    if (File.Exists(path))
    {
        var (dateCreated, dateLastAccessed, fileName, fileExtention, fileLength, hasError) = engine.GetFileProperties(selectedItem.FullName.ToString());
 
        if (!hasError)
        {
            txtFileName.Text = fileName;
            txtExtension.Text = fileExtention;
            dtCreated.Value = dateCreated;
            dtLastAccessed.Value = dateLastAccessed;
            txtFilePath.Text = selectedItem.FullName.ToString();
            txtFileSize.Text = $"{Round(fileLength.ToMegabytes(), 2).ToString()} MB";
        }
    }
}

Again, this is very marginally changed; instead of storing the full filename (with the path) in the node tag property, we're now just referencing the underlying model, so it's much clearer. Our next step is to remove the existing WinForms TreeView control.

Removing the existing TreeView

The following code should be removed from ImportBooks.Designer.cs:

// 
// tvFoundBooks
// 
this.tvFoundBooks.Location = new System.Drawing.Point(12, 41);
this.tvFoundBooks.Name = "tvFoundBooks";
this.tvFoundBooks.Size = new System.Drawing.Size(513, 246);
this.tvFoundBooks.TabIndex = 8;
this.tvFoundBooks.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.tvFoundBooks_AfterSelect);

This will remove the control itself. Later, we'll need to remove the following code that adds the TreeView to the controls collection:

this.Controls.Add(this.tvFoundBooks);

That's it. If you now run the project, you'll see a UWP TreeView control right in the middle of a WinForms application.