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.
- Data binding and MVVM - UWP applications | Microsoft Docs
- Patterns - WPF Apps With The Model-View-ViewModel Design Pattern | Microsoft Docs
MainWindow.xaml.cs
using System.Windows;
namespace PdfPrintSample.Main
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Add ViewModel
Now I need a "ViewModel" class.
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");
}
}
}
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"));
}
}
}
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();
}
}
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);
}
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;
}
}
}
}
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);
}
}
}
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>
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;
}
...
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();
}
}
}
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>
MainWindow.xaml.cs
using System.Windows;
namespace PdfPrintSample.Main
{
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
}
}
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"}"
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'}"
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;
...
}
}
...
Top comments (0)