DEV Community

loading...

【.NET 5】【WPF】Try MVVM

Masui Masanori
Programmer, husband, father I love C#, TypeScript, etc.
・5 min read

Intro

This time, I try MVVM to draw loaded the spreadsheet's data on the canvas.

Environments

  • .NET ver.5.0.101
  • Microsoft.Extensions.DependencyInjection ver.5.0.1
  • NLog ver.4.7.6
  • Microsoft.Xaml.Behaviors.Wpf ver.1.1.31
  • Newtonsoft.Json ver.12.0.3

MVVM

To use MVVM pattern, I remove most of functions from the code behind of MainWindow.xaml.

MainWindow.xaml.cs

using System.Windows;
namespace PdfPrintSample.Main
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Add ViewModel

Now I need a "ViewModel" class.
Alt Text

Events what are fired from MainWindow.xaml are handled by classes what implement "ICommand".
Properties what are binded in MainWindow.xaml are controlled by classes what implement "INotifyPropertyChanged".

And they are published by the "ViewModel" class.

LoadCommand.cs

using System;
using System.Windows.Input;
using NLog;
namespace PdfPrintSample.Main
{
    public class LoadCommand : ICommand
    {
        private readonly Logger logger = LogManager.GetCurrentClassLogger();
        public event EventHandler? CanExecuteChanged;
        public bool CanExecute(object? parameter)
        {
            return true;
        }
        public void Execute(object? parameter)
        {
            logger.Debug("Executed");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

WorksheetView.cs

using System.ComponentModel;
namespace PdfPrintSample.Main
{
    public class WorksheetView: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        private Spreadsheets.Values.Worksheet? worksheet = new Spreadsheets.Values.Worksheet(); 
        public Spreadsheets.Values.Worksheet? Worksheet => worksheet;
        public void Update(Spreadsheets.Values.Worksheet? value)
        {
            worksheet = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Worksheet"));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MainViewModel.cs

using System;
using NLog;
using PdfPrintSample.Spreadsheets;
namespace PdfPrintSample.Main
{
    public class MainViewModel
    {
        private readonly Logger logger = LogManager.GetCurrentClassLogger();
        public string Title { get; set; } = "Hello";
        public LoadCommand Load { get; set; } = new LoadCommand();
        public WorksheetView Worksheet { get; } = new WorksheetView();
    }
}
Enter fullscreen mode Exit fullscreen mode

How does the "ViewModel" class know the command is fired?

Even though MainWindow.xaml fires LoadCommand, MainViewModel can't know that.

So the LoadCommand needs actions and MainViewModel needs subscribe them.

LoadSpreadsheetArgs.cs

namespace PdfPrintSample.Spreadsheets.Values
{
    public record LoadSpreadsheetArgs(string FilePath, string SheetName);
}
Enter fullscreen mode Exit fullscreen mode

LoadCommand.cs

...
using PdfPrintSample.Spreadsheets.Values;
namespace PdfPrintSample.Main
{
    public class LoadCommand : ICommand
    {
...
        public Action<LoadSpreadsheetArgs>? LoadSpreadsheetNeeded;
...
        public void Execute(object? parameter)
        {
            // ex. dotnet run spreadsheet
            var args = Environment.GetCommandLineArgs();
            if(args.Length <= 1)
            {
                logger.Error("No arguments");
                return;
            }
            switch(args[1])
            {
                case "pdf":
                    logger.Debug("Load PDF");
                    // TODO: add an action for loading PDF
                    break;
                case "spreadsheet":
                    logger.Debug("Load Spreadsheet");
                    LoadSpreadsheetNeeded?.Invoke(new LoadSpreadsheetArgs("sample.xlsx", "Sheet1"));
                    break;
                default:
                    logger.Error("No actions");
                    break;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MainViewModel.cs

...
using PdfPrintSample.Spreadsheets.Values;
namespace PdfPrintSample.Main
{
    public class MainViewModel
    {
...
        public MainViewModel()
        {
            Load.LoadSpreadsheetNeeded += LoadSpreadsheet;
        }
        private void LoadSpreadsheet(LoadSpreadsheetArgs args)
        {
            // TODO: Load the spreadsheet.
            logger.Debug(args);            
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

Bind a comannd into the "ContentRendered" event

By default, I can't bind LoadCommand into the "ContentRendered" event.
Of cource, I can call the event from the code behind.
But I don't want do that.

So I install "Microsoft.Xaml.Behaviors.Wpf".

Previously, it had been called "System.Windows.Interactivity".
Most of the usage are same as before.

MainWindow.xaml

<Window x:Class="PdfPrintSample.Main.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:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:vm="clr-namespace:PdfPrintSample.Main"
        xmlns:local="clr-namespace:PdfPrintSample.Main"
        mc:Ignorable="d"
        Title="{Binding Title}" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    <Behaviors:Interaction.Triggers>
        <Behaviors:EventTrigger EventName="ContentRendered">
            <Behaviors:InvokeCommandAction Command="{Binding Load}"/>
        </Behaviors:EventTrigger>
    </Behaviors:Interaction.Triggers>
    <Grid>
    </Grid>
</Window>
Enter fullscreen mode Exit fullscreen mode

ViewModel with DI

I have still had a problem.
I can't inject the dependencies into the "ViewModel" class or I can't show MainWindow.

MainViewModel.cs

...
namespace PdfPrintSample.Main
{
    public class MainViewModel
    {
 ...
        private readonly ISpreadsheetLoader spreadsheets;
        // When this is binded by XAML, it can't resolve the dependencies
        public MainViewModel(ISpreadsheetLoader spreadsheets)
        {
            this.spreadsheets = spreadsheets;
            Load.LoadSpreadsheetNeeded += LoadSpreadsheet;
        }
...
Enter fullscreen mode Exit fullscreen mode

So I set DataContext of the MainWindow.xaml from the code behind.

App.xaml.cs

...
namespace PdfPrintSample
{
    public partial class App : Application
    {
...
        private static IServiceProvider BuildDi()
        {
            var services = new ServiceCollection();
            services.AddScoped<MainWindow>();
            services.AddScoped<MainViewModel>();
            services.AddScoped<ISpreadsheetLoader, SpreadsheetLoader>();
            return services.BuildServiceProvider();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

MainWindow.xaml

<Window x:Class="PdfPrintSample.Main.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:Behaviors="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:vm="clr-namespace:PdfPrintSample.Main"
        xmlns:local="clr-namespace:PdfPrintSample.Main"
        mc:Ignorable="d"
        Title="{Binding Title}" Height="450" Width="800">
    <!-- set from the code behind -->
    <!--Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext -->
    <Behaviors:Interaction.Triggers>
        <Behaviors:EventTrigger EventName="ContentRendered">
            <Behaviors:InvokeCommandAction Command="{Binding Load}"/>
        </Behaviors:EventTrigger>
    </Behaviors:Interaction.Triggers>
    <Grid>
    </Grid>
</Window>
Enter fullscreen mode Exit fullscreen mode

MainWindow.xaml.cs

using System.Windows;
namespace PdfPrintSample.Main
{
    public partial class MainWindow : Window
    {
        public MainWindow(MainViewModel viewModel)
        {
            DataContext = viewModel;
            InitializeComponent();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Get and deserialize JSON from command line arguments

Because I don't want to add many command line arguments, I try treating JSON as a command line argument.

Use single quotations

I can't use double quotations into the JSON text like below.

dotnet run "{"filePath":"sample.xlsx","sheetName":"Sheet1"}"
Enter fullscreen mode Exit fullscreen mode

Or it will be separated by the double quotations and I will only can get "{" from the command line arguments.

So I have to use single quotations.

dotnet run "{'filePath':'sample.xlsx','sheetName':'Sheet1'}"
Enter fullscreen mode Exit fullscreen mode

Deserialize

After getting the JSON text, I just deserialize it.
But I can't use "System.Text.Json" or I will get exceptions.

So I use "Newtonsoft.Json".

LoadCommand.cs

...
using PdfPrintSample.Spreadsheets.Values;
namespace PdfPrintSample.Main
{
    public class LoadCommand : ICommand
    {
...
        public void Execute(object? parameter)
        {
            // ex. dotnet run spreadsheet "{'filePath':'sample.xlsx','sheetName':'Sheet1'}"
            var args = Environment.GetCommandLineArgs();
            if(args.Length <= 2)
            {
                logger.Error("No arguments");
                return;
            }
            switch(args[1])
            {
                case "pdf":
                    logger.Debug("Load PDF");
                    // TODO: add an action for loading PDF
                    break;
                case "spreadsheet":
                    logger.Debug("Load Spreadsheet");
                    LoadSpreadsheetNeeded?.Invoke(
                        JsonConvert.DeserializeObject<LoadSpreadsheetArgs>(args[2]));
                    break;
...
            }
        }
...
Enter fullscreen mode Exit fullscreen mode

Discussion (0)