From 29a118172ea97fea9d630954cf691a981fbd3da6 Mon Sep 17 00:00:00 2001
From: VladimirKhil <vladimir.khil@gmail.com>
Date: Sun, 29 Oct 2023 22:12:44 +0100
Subject: [PATCH] https://github.com/VladimirKhil/SI/issues/18 Make
 SIQuester.ViewModel cross-platform; enable new format for new packages; add
 more localization

---
 .../Contracts/Host/IClipboardService.cs       |  6 ++
 .../Contracts/IDocumentViewModelFactory.cs    | 16 ++++
 .../SIQuester.ViewModel/ModelViewBase.cs      | 16 ----
 .../PlatformSpecific/PlatformManager.cs       |  5 ++
 .../SIQuester.ViewModel.csproj                |  3 +-
 .../ServiceCollectionExtensions.cs            | 26 ++++++
 .../Services/DocumentViewModelFactory.cs      | 29 ++++++
 .../Services/PackageTemplatesRepository.cs    |  2 +-
 .../Dialogs/SelectThemesViewModel.cs          | 14 +--
 .../Workspaces/ImportDBStorageViewModel.cs    | 21 +++--
 .../Workspaces/ImportSIStorageViewModel.cs    | 19 ++--
 .../Workspaces/ImportTextViewModel.cs         | 17 ++--
 .../Workspaces/MainViewModel.cs               | 90 ++++++++-----------
 .../Workspaces/NewViewModel.cs                |  8 +-
 .../Workspaces/QDocument.cs                   | 37 ++++++--
 .../Workspaces/SettingsViewModel.cs           |  3 +-
 src/SIQuester/SIQuester/App.xaml.cs           | 17 +++-
 .../Behaviors/CommandBindingsManager.cs       | 44 ---------
 .../Helpers/DocumentCollectionController.cs   | 17 ++++
 .../Implementation/DesktopManager.cs          |  2 +
 src/SIQuester/SIQuester/MainWindow.xaml       | 49 +++++-----
 src/SIQuester/SIQuester/MainWindow.xaml.cs    | 19 ++++
 .../Properties/Resources.Designer.cs          | 36 ++++++++
 .../SIQuester/Properties/Resources.en-US.resx | 12 +++
 .../SIQuester/Properties/Resources.resx       | 12 +++
 src/SIQuester/SIQuester/SIQuester.csproj      |  1 +
 .../Services/Host/ClipboardService.cs         |  3 +
 .../SIQuester/View/SpardEditorView.xaml       | 23 ++---
 src/SIQuester/SIQuester/appsettings.json      |  2 +-
 29 files changed, 346 insertions(+), 203 deletions(-)
 create mode 100644 src/SIQuester/SIQuester.ViewModel/Contracts/IDocumentViewModelFactory.cs
 create mode 100644 src/SIQuester/SIQuester.ViewModel/ServiceCollectionExtensions.cs
 create mode 100644 src/SIQuester/SIQuester.ViewModel/Services/DocumentViewModelFactory.cs
 delete mode 100644 src/SIQuester/SIQuester/Behaviors/CommandBindingsManager.cs
 create mode 100644 src/SIQuester/SIQuester/Helpers/DocumentCollectionController.cs

diff --git a/src/SIQuester/SIQuester.ViewModel/Contracts/Host/IClipboardService.cs b/src/SIQuester/SIQuester.ViewModel/Contracts/Host/IClipboardService.cs
index 5c49befb..00040995 100644
--- a/src/SIQuester/SIQuester.ViewModel/Contracts/Host/IClipboardService.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Contracts/Host/IClipboardService.cs
@@ -5,6 +5,12 @@
 /// </summary>
 public interface IClipboardService
 {
+    /// <summary>
+    /// Queries the Clipboard for the presence of data in a specified data format.
+    /// </summary>
+    /// <param name="format">Data format.</param>
+    bool ContainsData(string format);
+
     /// <summary>
     /// Retrieves data in a specified format from the Clipboard.
     /// </summary>
diff --git a/src/SIQuester/SIQuester.ViewModel/Contracts/IDocumentViewModelFactory.cs b/src/SIQuester/SIQuester.ViewModel/Contracts/IDocumentViewModelFactory.cs
new file mode 100644
index 00000000..c6c82264
--- /dev/null
+++ b/src/SIQuester/SIQuester.ViewModel/Contracts/IDocumentViewModelFactory.cs
@@ -0,0 +1,16 @@
+using SIPackages;
+
+namespace SIQuester.ViewModel.Contracts;
+
+/// <summary>
+/// Provides method for creating documents.
+/// </summary>
+public interface IDocumentViewModelFactory
+{
+    /// <summary>
+    /// Creates view model for the document.
+    /// </summary>
+    /// <param name="document">Package document.</param>
+    /// <param name="fileName">Package file name.</param>
+    QDocument CreateViewModelFor(SIDocument document, string? fileName = null);
+}
diff --git a/src/SIQuester/SIQuester.ViewModel/ModelViewBase.cs b/src/SIQuester/SIQuester.ViewModel/ModelViewBase.cs
index fe62e1e1..068e93cf 100644
--- a/src/SIQuester/SIQuester.ViewModel/ModelViewBase.cs
+++ b/src/SIQuester/SIQuester.ViewModel/ModelViewBase.cs
@@ -1,7 +1,6 @@
 using SIPackages.Core;
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
-using System.Windows.Input;
 
 namespace SIQuester.ViewModel;
 
@@ -10,21 +9,6 @@ namespace SIQuester.ViewModel;
 /// </summary>
 public abstract class ModelViewBase : INotifyPropertyChanged, IDisposable
 {
-    /// <summary>
-    /// Allows to keep bindings to common application commands.
-    /// </summary>
-    public CommandBindingCollection CommandBindings { get; } = new();
-
-    protected void AddCommandBinding(ICommand command, ExecutedRoutedEventHandler executed, CanExecuteRoutedEventHandler canExecute = null)
-    {
-        var commandBinding = canExecute != null ?
-            new CommandBinding(command, executed, canExecute)
-            : new CommandBinding(command, executed);
-
-        CommandManager.RegisterClassCommandBinding(GetType(), commandBinding);
-        CommandBindings.Add(commandBinding);
-    }
-
     protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
 
diff --git a/src/SIQuester/SIQuester.ViewModel/PlatformSpecific/PlatformManager.cs b/src/SIQuester/SIQuester.ViewModel/PlatformSpecific/PlatformManager.cs
index 5ad0f0d8..6e181b32 100644
--- a/src/SIQuester/SIQuester.ViewModel/PlatformSpecific/PlatformManager.cs
+++ b/src/SIQuester/SIQuester.ViewModel/PlatformSpecific/PlatformManager.cs
@@ -16,6 +16,11 @@ public abstract class PlatformManager
 
     public IServiceProvider ServiceProvider { get; set; }
 
+    /// <summary>
+    /// Gets well-known font family names.
+    /// </summary>
+    public abstract string[] FontFamilies { get; }
+
     protected PlatformManager()
     {
         Instance = this;
diff --git a/src/SIQuester/SIQuester.ViewModel/SIQuester.ViewModel.csproj b/src/SIQuester/SIQuester.ViewModel/SIQuester.ViewModel.csproj
index cd639f8c..1b31668a 100644
--- a/src/SIQuester/SIQuester.ViewModel/SIQuester.ViewModel.csproj
+++ b/src/SIQuester/SIQuester.ViewModel/SIQuester.ViewModel.csproj
@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net6.0-windows</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <AssemblyTitle>SIQuester.ViewModel</AssemblyTitle>
     <Product>SIQuester.ViewModel</Product>
     <Description>SIQuester business logic</Description>
@@ -32,7 +32,6 @@
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
     <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
-	<PackageReference Include="MahApps.Metro" Version="2.4.10" />
     <PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
 	<PackageReference Include="YamlDotNet" Version="13.1.1" />
   </ItemGroup>
diff --git a/src/SIQuester/SIQuester.ViewModel/ServiceCollectionExtensions.cs b/src/SIQuester/SIQuester.ViewModel/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..3c874d49
--- /dev/null
+++ b/src/SIQuester/SIQuester.ViewModel/ServiceCollectionExtensions.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using SIQuester.ViewModel.Contracts;
+using SIQuester.ViewModel.Services;
+using SIStorageService.ViewModel;
+
+namespace SIQuester.ViewModel;
+
+/// <summary>
+/// Allows to register SIQuester view model in <see cref="IServiceCollection" />.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+    /// <summary>
+    /// Registers SIQuester view model in <see cref="IServiceCollection" />.
+    /// </summary>
+    /// <param name="services">Services collection.</param>
+    public static IServiceCollection AddSIQuester(this IServiceCollection services)
+    {
+        services.AddSingleton<IPackageTemplatesRepository, PackageTemplatesRepository>();
+        services.AddSingleton<StorageViewModel>();
+        services.AddSingleton<StorageContextViewModel>();
+        services.AddSingleton<IDocumentViewModelFactory, DocumentViewModelFactory>();
+
+        return services;
+    }
+}
diff --git a/src/SIQuester/SIQuester.ViewModel/Services/DocumentViewModelFactory.cs b/src/SIQuester/SIQuester.ViewModel/Services/DocumentViewModelFactory.cs
new file mode 100644
index 00000000..39c853c6
--- /dev/null
+++ b/src/SIQuester/SIQuester.ViewModel/Services/DocumentViewModelFactory.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Logging;
+using SIPackages;
+using SIQuester.ViewModel.Contracts;
+using SIQuester.ViewModel.Contracts.Host;
+
+namespace SIQuester.ViewModel.Services;
+
+/// <inheritdoc />
+internal class DocumentViewModelFactory : IDocumentViewModelFactory
+{
+    private readonly StorageContextViewModel _storageContextViewModel;
+    private readonly IClipboardService _clipboardService;
+    private readonly ILoggerFactory _loggerFactory;
+
+    public DocumentViewModelFactory(
+        StorageContextViewModel storageContextViewModel,
+        IClipboardService clipboardService,
+        ILoggerFactory loggerFactory)
+    {
+        _storageContextViewModel = storageContextViewModel;
+        _clipboardService = clipboardService;
+        _loggerFactory = loggerFactory;
+    }
+
+    public QDocument CreateViewModelFor(SIDocument document, string? fileName = null) => new(document, _storageContextViewModel, _clipboardService, _loggerFactory)
+    {
+        FileName = fileName ?? document.Package.Name
+    };
+}
diff --git a/src/SIQuester/SIQuester.ViewModel/Services/PackageTemplatesRepository.cs b/src/SIQuester/SIQuester.ViewModel/Services/PackageTemplatesRepository.cs
index bcef9b34..6ba4dda8 100644
--- a/src/SIQuester/SIQuester.ViewModel/Services/PackageTemplatesRepository.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Services/PackageTemplatesRepository.cs
@@ -4,7 +4,7 @@
 
 namespace SIQuester.ViewModel.Services;
 
-/// <inheritdoc />
+/// <inheritdoc cref="IPackageTemplatesRepository" />
 public sealed class PackageTemplatesRepository : IPackageTemplatesRepository
 {
     /// <summary>
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/Dialogs/SelectThemesViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/Dialogs/SelectThemesViewModel.cs
index ab4f6f69..5545e28e 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/Dialogs/SelectThemesViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/Dialogs/SelectThemesViewModel.cs
@@ -1,8 +1,8 @@
-using Microsoft.Extensions.Logging;
-using SIPackages;
+using SIPackages;
 using SIPackages.Core;
 using SIQuester.Model;
 using SIQuester.ViewModel.Configuration;
+using SIQuester.ViewModel.Contracts;
 using SIQuester.ViewModel.Properties;
 using System.Windows.Input;
 using Utils.Commands;
@@ -57,13 +57,13 @@ public int To
 
     public ICommand Select2 { get; private set; }
 
-    private readonly ILoggerFactory _loggerFactory;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
 
-    public SelectThemesViewModel(QDocument document, AppOptions appOptions, ILoggerFactory loggerFactory)
+    public SelectThemesViewModel(QDocument document, AppOptions appOptions, IDocumentViewModelFactory documentViewModelFactory)
     {
         _document = document;
         _appOptions = appOptions;
-        _loggerFactory = loggerFactory;
+        _documentViewModelFactory = documentViewModelFactory;
 
         Themes = _document.Document.Package.Rounds
             .SelectMany(round => round.Themes)
@@ -86,7 +86,7 @@ private async void Select_Executed(object? arg)
             var allthemes = new List<Theme>();
             _document.Document.Package.Rounds.ForEach(round => round.Themes.ForEach(allthemes.Add));
 
-            var targetDocument = new QDocument(newDocument, _document.StorageContext, _loggerFactory) { FileName = newDocument.Package.Name };
+            var targetDocument = _documentViewModelFactory.CreateViewModelFor(newDocument);
 
             for (var index = _from; index <= _to; index++)
             {
@@ -120,7 +120,7 @@ private async void Select2_Executed(object? arg)
 
             var allthemes = Themes.Where(st => st.IsSelected).Select(st => st.Theme);
 
-            var targetDocument = new QDocument(newDocument, _document.StorageContext, _loggerFactory) { FileName = newDocument.Package.Name };
+            var targetDocument = _documentViewModelFactory.CreateViewModelFor(newDocument);
 
             foreach (var theme in allthemes)
             {
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportDBStorageViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportDBStorageViewModel.cs
index f46e3653..1b981588 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportDBStorageViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportDBStorageViewModel.cs
@@ -1,5 +1,4 @@
-using Microsoft.Extensions.Logging;
-using Notions;
+using Notions;
 using SIPackages;
 using SIPackages.Core;
 using SIQuester.ViewModel.Configuration;
@@ -71,29 +70,29 @@ private async void LoadTours()
         }
     }
 
-    private readonly StorageContextViewModel _storageContextViewModel;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
     private readonly AppOptions _appOptions;
     private readonly IChgkDbClient _chgkDbClient;
-    private readonly ILoggerFactory _loggerFactory;
 
     public ImportDBStorageViewModel(
-        StorageContextViewModel storageContextViewModel,
+        IDocumentViewModelFactory documentViewModelFactory,
         IChgkDbClient chgkDbClient,
-        AppOptions appOptions,
-        ILoggerFactory loggerFactory)
+        AppOptions appOptions)
     {
-        _storageContextViewModel = storageContextViewModel;
+        _documentViewModelFactory = documentViewModelFactory;
         _appOptions = appOptions;
         _chgkDbClient = chgkDbClient;
-        _loggerFactory = loggerFactory;
     }
 
     public async Task SelectNodeAsync(DBNode item)
     {
         async Task<QDocument> loader(CancellationToken cancellationToken)
         {
-            var siDoc = await SelectAsync(item, cancellationToken);
-            return new QDocument(siDoc, _storageContextViewModel, _loggerFactory) { FileName = siDoc.Package.Name, Changed = true };
+            var siDocument = await SelectAsync(item, cancellationToken);
+            var documentViewModel = _documentViewModelFactory.CreateViewModelFor(siDocument);
+            documentViewModel.Changed = true;
+
+            return documentViewModel;
         };
 
         var loaderViewModel = new DocumentLoaderViewModel(item.Name);
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportSIStorageViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportSIStorageViewModel.cs
index b11b070b..6e74ca58 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportSIStorageViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportSIStorageViewModel.cs
@@ -1,5 +1,5 @@
-using Microsoft.Extensions.Logging;
-using SIQuester.ViewModel.Configuration;
+using SIQuester.ViewModel.Configuration;
+using SIQuester.ViewModel.Contracts;
 using SIQuester.ViewModel.Properties;
 using SIStorageService.ViewModel;
 using System.ComponentModel;
@@ -7,6 +7,9 @@
 
 namespace SIQuester.ViewModel;
 
+/// <summary>
+/// Allows to import package from SIStorage.
+/// </summary>
 public sealed class ImportSIStorageViewModel : WorkspaceViewModel
 {
     private static readonly HttpClient HttpClient = new() { DefaultRequestVersion = HttpVersion.Version20 };
@@ -15,24 +18,20 @@ public sealed class ImportSIStorageViewModel : WorkspaceViewModel
 
     public override string Header => Resources.SIStorage;
 
-    private readonly StorageContextViewModel _storageContextViewModel;
-
     public bool IsProgress => Storage.IsLoading || Storage.IsLoadingPackages;
 
     private readonly AppOptions _appOptions;
-    private readonly ILoggerFactory _loggerFactory;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
 
     private readonly CancellationTokenSource _cancellationTokenSource = new();
 
     public ImportSIStorageViewModel(
-        StorageContextViewModel storageContextViewModel,
         StorageViewModel siStorage,
         AppOptions appOptions,
-        ILoggerFactory loggerFactory)
+        IDocumentViewModelFactory documentViewModelFactory)
     {
-        _storageContextViewModel = storageContextViewModel;
         _appOptions = appOptions;
-        _loggerFactory = loggerFactory;
+        _documentViewModelFactory = documentViewModelFactory;
 
         Storage = siStorage;
 
@@ -78,7 +77,7 @@ async Task<QDocument> loader(Uri uri, CancellationToken cancellationToken)
                 doc.Upgrade();
             }
 
-            return new QDocument(doc, _storageContextViewModel, _loggerFactory) { FileName = doc.Package.Name };
+            return _documentViewModelFactory.CreateViewModelFor(doc);
         };
 
         var package = Storage.CurrentPackage;
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportTextViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportTextViewModel.cs
index 42d52bad..ac40a86a 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportTextViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/ImportTextViewModel.cs
@@ -260,7 +260,6 @@ public int Progress
     private readonly CancellationTokenSource _tokenSource = new();
     private readonly TaskScheduler _scheduler;
 
-    private readonly StorageContextViewModel _storageContextViewModel;
     private readonly AppOptions _appOptions;
     private string _badTextCopy = "";
 
@@ -352,7 +351,7 @@ public string SkipToolTip
 
     public event Action<int, int, string?, bool>? HighlightText;
 
-    private readonly ILoggerFactory _loggerFactory;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
 
     private Encoding _textEncoding = Encoding.UTF8;
 
@@ -377,15 +376,13 @@ public Encoding TextEncoding
     /// <summary>
     /// Initializes a new instance of <see cref="ImportTextViewModel" /> class.
     /// </summary>
-    /// <param name="storageContextViewModel">Well-known SIStorage facets holder.</param>
     /// <param name="appOptions">Application options.</param>
     /// <param name="clipboardService">Clipboard access service.</param>
-    /// <param name="loggerFactory">Factory to create loggers.</param>
-    public ImportTextViewModel(StorageContextViewModel storageContextViewModel, AppOptions appOptions, IClipboardService clipboardService, ILoggerFactory loggerFactory)
+    /// <param name="documentViewModelFactory">Factory to create documents.</param>
+    public ImportTextViewModel(AppOptions appOptions, IClipboardService clipboardService, IDocumentViewModelFactory documentViewModelFactory)
     {
-        _storageContextViewModel = storageContextViewModel;
         _appOptions = appOptions;
-        _loggerFactory = loggerFactory;
+        _documentViewModelFactory = documentViewModelFactory;
         _scheduler = TaskScheduler.FromCurrentSynchronizationContext();
 
         var trashAlias = new EditAlias(Resources.Trash, "#FFD3D3D3");
@@ -569,10 +566,10 @@ private void AnalyzeFinished(Task<Tuple<bool, int>> task)
             var themesNum = task.Result.Item2;
             if (task.Result.Item1)
             {
-                if (!task.IsCanceled)
+                if (!task.IsCanceled && _existing != null)
                 {
                     PlatformManager.Instance.Inform($"{Resources.Success} {themesNum}.");
-                    OnNewItem(new QDocument(_existing, _storageContextViewModel, _loggerFactory) { FileName = _existing.Package.Name });
+                    OnNewItem(_documentViewModelFactory.CreateViewModelFor(_existing));
                 }
             }
         }
@@ -825,7 +822,7 @@ public void Clean()
                     exc => OnError(exc, ""),
                     CancellationToken.None);
 
-                OnNewItem(new QDocument(_existing, _storageContextViewModel, _loggerFactory) { FileName = _existing.Package.Name });
+                OnNewItem(_documentViewModelFactory.CreateViewModelFor(_existing));
             }
         }
 
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/MainViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/MainViewModel.cs
index c174e19b..0611a58a 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/MainViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/MainViewModel.cs
@@ -11,13 +11,11 @@
 using SIQuester.ViewModel.Properties;
 using SIQuester.ViewModel.Serializers;
 using SIQuester.ViewModel.Services;
-using SIStorage.Service.Contract;
 using SIStorageService.ViewModel;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Text;
-using System.Windows.Data;
 using System.Windows.Input;
 using Utils;
 using Utils.Commands;
@@ -38,6 +36,11 @@ public sealed class MainViewModel : ModelViewBase, INotifyPropertyChanged
 
     #region Commands
 
+    /// <summary>
+    /// Creates a new workspace.
+    /// </summary>
+    public ICommand New { get; private set; }
+
     /// <summary>
     /// Opens a file.
     /// </summary>
@@ -93,6 +96,16 @@ public sealed class MainViewModel : ModelViewBase, INotifyPropertyChanged
 
     public ICommand SearchFolder { get; private set; }
 
+    /// <summary>
+    /// Opens a help file.
+    /// </summary>
+    public ICommand Help { get; private set; }
+
+    /// <summary>
+    /// Closes main view.
+    /// </summary>
+    public IAsyncCommand Close { get; private set; }
+
     #endregion
 
     /// <summary>
@@ -122,8 +135,8 @@ public QDocument? ActiveDocument
 
     private readonly string[] _args;
     private readonly AppOptions _appOptions;
-    private readonly StorageContextViewModel _storageContextViewModel;
     private readonly IClipboardService _clipboardService;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
     private readonly IServiceProvider _serviceProvider;
 
     public AppOptions AppOptions => _appOptions;
@@ -131,18 +144,18 @@ public QDocument? ActiveDocument
     public MainViewModel(
         string[] args,
         AppOptions appOptions,
-        ISIStorageServiceClient siStorageServiceClient,
         IClipboardService clipboardService,
         IServiceProvider serviceProvider,
+        IDocumentViewModelFactory documentViewModelFactory,
         ILoggerFactory loggerFactory)
     {
         _loggerFactory = loggerFactory;
         _clipboardService = clipboardService;
+        _documentViewModelFactory = documentViewModelFactory;
         _logger = loggerFactory.CreateLogger<MainViewModel>();
         _appOptions = appOptions;
 
         DocList.CollectionChanged += DocList_CollectionChanged;
-        CollectionViewSource.GetDefaultView(DocList).CurrentChanged += MainViewModel_CurrentChanged;
 
         Open = new SimpleCommand(Open_Executed);
         OpenRecent = new SimpleCommand(OpenRecent_Executed);
@@ -163,28 +176,17 @@ public MainViewModel(
         SetSettings = new SimpleCommand(SetSettings_Executed);
         SearchFolder = new SimpleCommand(SearchFolder_Executed);
 
-        _storageContextViewModel = new StorageContextViewModel(siStorageServiceClient);
-        _storageContextViewModel.Load();
-
         _serviceProvider = serviceProvider;
 
-        AddCommandBinding(ApplicationCommands.New, New_Executed);
-        AddCommandBinding(ApplicationCommands.Open, (sender, e) => Open_Executed(e.Parameter));
-        AddCommandBinding(ApplicationCommands.Help, Help_Executed);
-        AddCommandBinding(ApplicationCommands.Close, Close_Executed);
-
-        AddCommandBinding(ApplicationCommands.SaveAs, (s, e) => ActiveDocument?.SaveAs_Executed(), CanExecuteDocumentCommand);
-
-        AddCommandBinding(ApplicationCommands.Copy, (s, e) => ActiveDocument?.Copy_Executed(), CanExecuteDocumentCommand);
-        AddCommandBinding(ApplicationCommands.Paste, (s, e) => ActiveDocument?.Paste_Executed(), CanExecuteDocumentCommand);
+        New = new SimpleCommand(New_Executed);
+        Help = new SimpleCommand(Help_Executed);
+        Close = new AsyncCommand(Close_Executed);
 
         _args = args;
 
         UI.Initialize();
     }
 
-    private void CanExecuteDocumentCommand(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = ActiveDocument != null;
-
     public async Task InitializeAsync()
     {
         if (_args.Length > 0)
@@ -244,9 +246,9 @@ public async Task InitializeAsync()
 
     private void SetSettings_Executed(object? arg) => DocList.Add(new SettingsViewModel());
 
-    private void Help_Executed(object? sender, ExecutedRoutedEventArgs e) => PlatformManager.Instance.ShowHelp();
+    private void Help_Executed(object? arg) => PlatformManager.Instance.ShowHelp();
 
-    private async void Close_Executed(object? sender, ExecutedRoutedEventArgs e)
+    private async Task Close_Executed(object? arg)
     {
         _logger.LogInformation("Close_Executed");
 
@@ -294,9 +296,6 @@ private static void OpenUri(string uri)
 
     private void About_Executed(object? arg) => DocList.Add(new AboutViewModel());
 
-    private void MainViewModel_CurrentChanged(object? sender, EventArgs e) =>
-        ActiveDocument = CollectionViewSource.GetDefaultView(DocList).CurrentItem as QDocument;
-
     private void DocList_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
         switch (e.Action)
@@ -309,7 +308,6 @@ private void DocList_CollectionChanged(object? sender, NotifyCollectionChangedEv
                     item.Closed += Item_Closed;
                 }
 
-                CollectionViewSource.GetDefaultView(DocList).MoveCurrentToLast();
                 CheckSaveAllCanBeExecuted(this, EventArgs.Empty);
                 break;
 
@@ -351,10 +349,8 @@ private void CheckSaveAllCanBeExecuted(object sender, EventArgs e)
     /// <summary>
     /// Новый
     /// </summary>
-    private void New_Executed(object? sender, ExecutedRoutedEventArgs e)
-    {
-        DocList.Add(new NewViewModel(_storageContextViewModel, _serviceProvider.GetRequiredService<IPackageTemplatesRepository>(), _appOptions, _loggerFactory));
-    }
+    private void New_Executed(object? arg) =>
+        DocList.Add(new NewViewModel(_serviceProvider.GetRequiredService<IPackageTemplatesRepository>(), _appOptions, _documentViewModelFactory, _loggerFactory));
 
     /// <summary>
     /// Открыть существующий пакет
@@ -426,11 +422,8 @@ Task<QDocument> loader(CancellationToken cancellationToken) => Task.Run(() =>
 
                 _logger.LogInformation("Document has been successfully opened. Path: {path}", path);
 
-                var docViewModel = new QDocument(doc, _storageContextViewModel, _loggerFactory)
-                {
-                    Path = path,
-                    FileName = Path.GetFileNameWithoutExtension(path)
-                };
+                var docViewModel = _documentViewModelFactory.CreateViewModelFor(doc, Path.GetFileNameWithoutExtension(path));
+                docViewModel.Path = path;
 
                 docViewModel.CheckFileSize();
 
@@ -522,7 +515,7 @@ private void ImportTxt_Executed(object? arg)
                 _ => throw new InvalidOperationException($"Incorrect text source: {arg}"),
             };
 
-            var model = new ImportTextViewModel(_storageContextViewModel, _appOptions, _clipboardService, _loggerFactory);
+            var model = new ImportTextViewModel(_appOptions, _clipboardService, _documentViewModelFactory);
             DocList.Add(model);
 
             if (textSource != null)
@@ -550,12 +543,10 @@ private void ImportXml_Executed(object? arg)
             using var stream = File.OpenRead(file);
             var doc = SIDocument.LoadXml(stream);
 
-            var docViewModel = new QDocument(doc, _storageContextViewModel, _loggerFactory)
-            {
-                Path = "",
-                Changed = true,
-                FileName = Path.GetFileNameWithoutExtension(file)
-            };
+            var docViewModel = _documentViewModelFactory.CreateViewModelFor(doc, Path.GetFileNameWithoutExtension(file));
+
+            docViewModel.Path = "";
+            docViewModel.Changed = true;
 
             var mediaFolder = Path.GetDirectoryName(file);
 
@@ -597,12 +588,9 @@ private void ImportYaml_Executed(object? arg)
                 doc.Upgrade();
             }
 
-            var docViewModel = new QDocument(doc, _storageContextViewModel, _loggerFactory)
-            {
-                Path = "",
-                Changed = true,
-                FileName = Path.GetFileNameWithoutExtension(file)
-            };
+            var docViewModel = _documentViewModelFactory.CreateViewModelFor(doc, Path.GetFileNameWithoutExtension(file));
+            docViewModel.Path = "";
+            docViewModel.Changed = true;
 
             var mediaFolder = Path.GetDirectoryName(file);
 
@@ -624,10 +612,9 @@ private void ImportYaml_Executed(object? arg)
     /// </summary>
     private void ImportBase_Executed(object? arg) =>
         DocList.Add(new ImportDBStorageViewModel(
-            _storageContextViewModel,
+            _documentViewModelFactory,
             _serviceProvider.GetRequiredService<IChgkDbClient>(),
-            _appOptions,
-            _loggerFactory));
+            _appOptions));
 
     /// <summary>
     /// Imports package from SI Storage.
@@ -635,10 +622,9 @@ private void ImportBase_Executed(object? arg) =>
     private async void ImportFromSIStore_Executed(object? arg)
     {
         var importViewModel = new ImportSIStorageViewModel(
-            _storageContextViewModel,
             _serviceProvider.GetRequiredService<StorageViewModel>(),
             _appOptions,
-            _loggerFactory);
+            _documentViewModelFactory);
 
         DocList.Add(importViewModel);
 
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/NewViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/NewViewModel.cs
index d13b48fe..8d160c61 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/NewViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/NewViewModel.cs
@@ -88,21 +88,21 @@ public string PackageAuthor
     /// </summary>
     public List<string> Errors { get; } = new();
 
-    private readonly StorageContextViewModel _storageContextViewModel;
     private readonly IPackageTemplatesRepository _packageTemplatesRepository;
     private readonly AppOptions _appOptions;
     private readonly ILoggerFactory _loggerFactory;
+    private readonly IDocumentViewModelFactory _documentViewModelFactory;
     private readonly ILogger<NewViewModel> _logger;
 
     public NewViewModel(
-        StorageContextViewModel storageContextViewModel,
         IPackageTemplatesRepository packageTemplatesRepository,
         AppOptions appOptions,
+        IDocumentViewModelFactory documentViewModelFactory,
         ILoggerFactory loggerFactory)
     {
-        _storageContextViewModel = storageContextViewModel;
         _packageTemplatesRepository = packageTemplatesRepository;
         _appOptions = appOptions;
+        _documentViewModelFactory = documentViewModelFactory;
         _loggerFactory = loggerFactory;
         _logger = _loggerFactory.CreateLogger<NewViewModel>();
 
@@ -167,7 +167,7 @@ private void Create_Executed(object? arg)
                 siDocument.Upgrade();
             }
 
-            OnNewItem(new QDocument(siDocument, _storageContextViewModel, _loggerFactory) { FileName = siDocument.Package.Name });
+            OnNewItem(_documentViewModelFactory.CreateViewModelFor(siDocument));
             _logger.LogInformation("New document created. Name: {name}", siDocument.Package.Name);
             OnClosed();
         }
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/QDocument.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/QDocument.cs
index 00e641f5..fd67192e 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/QDocument.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/QDocument.cs
@@ -8,6 +8,7 @@
 using SIQuester.Model;
 using SIQuester.ViewModel.Configuration;
 using SIQuester.ViewModel.Contracts;
+using SIQuester.ViewModel.Contracts.Host;
 using SIQuester.ViewModel.Helpers;
 using SIQuester.ViewModel.Model;
 using SIQuester.ViewModel.PlatformSpecific;
@@ -59,6 +60,21 @@ public sealed class QDocument : WorkspaceViewModel
 
     private bool _changed = false;
 
+    /// <summary>
+    /// Saves document under different name.
+    /// </summary>
+    public IAsyncCommand SaveAs { get; private set; }
+
+    /// <summary>
+    /// Copies document item.
+    /// </summary>
+    public ICommand Copy { get; private set; }
+
+    /// <summary>
+    /// Pastes document item.
+    /// </summary>
+    public ICommand Paste { get; private set; }
+
     public OperationsManager OperationsManager { get; } = new();
 
     private IItemViewModel? _activeNode = null;
@@ -1202,11 +1218,13 @@ private void DetachParameterListeners(StepParameterRecord parameter)
 
     private bool _isLinksClearingBlocked;
 
+    private readonly IClipboardService _clipboardService;
     private readonly ILoggerFactory _loggerFactory;
 
     internal QDocument(
         SIDocument document,
         StorageContextViewModel storageContextViewModel,
+        IClipboardService clipboardService,
         ILoggerFactory loggerFactory)
     {
         Lock = new Lock(document.Package.Name);
@@ -1214,6 +1232,7 @@ internal QDocument(
         OperationsManager.Changed += OperationsManager_Changed;
         OperationsManager.Error += OperationsManager_Error;
 
+        _clipboardService = clipboardService;
         _loggerFactory = loggerFactory;
         _logger = loggerFactory.CreateLogger<QDocument>();
 
@@ -1222,6 +1241,7 @@ internal QDocument(
         ImportSiq = new SimpleCommand(ImportSiq_Executed);
         
         Save = new AsyncCommand(Save_Executed);
+        SaveAs = new AsyncCommand(SaveAs_Executed);
         SaveAsTemplate = new AsyncCommand(SaveAsTemplate_Executed);
 
         ExportHtml = new SimpleCommand(ExportHtml_Executed);
@@ -1256,6 +1276,9 @@ internal QDocument(
 
         Delete = new SimpleCommand(Delete_Executed);
 
+        Copy = new SimpleCommand(Copy_Executed);
+        Paste = new SimpleCommand(Paste_Executed);
+
         NextSearchResult = new SimpleCommand(NextSearchResult_Executed) { CanBeExecuted = false };
         PreviousSearchResult = new SimpleCommand(PreviousSearchResult_Executed) { CanBeExecuted = false };
         ClearSearchText = new SimpleCommand(ClearSearchText_Executed) { CanBeExecuted = false };
@@ -1545,7 +1568,7 @@ private static void CheckCommonFiles(ICollection<string> images, ICollection<str
         }
     }
 
-    internal void Copy_Executed()
+    internal void Copy_Executed(object? arg)
     {
         if (_activeNode == null)
         {
@@ -1555,7 +1578,7 @@ internal void Copy_Executed()
         try
         {
             var itemData = new InfoOwnerData(this, _activeNode);
-            Clipboard.SetData(ClipboardKey, itemData);
+            _clipboardService.SetData(ClipboardKey, itemData);
         }
         catch (Exception exc)
         {
@@ -1563,14 +1586,14 @@ internal void Copy_Executed()
         }
     }
 
-    internal void Paste_Executed()
+    internal void Paste_Executed(object? arg)
     {
         if (_activeNode == null)
         {
             return;
         }
 
-        if (!Clipboard.ContainsData(ClipboardKey))
+        if (!_clipboardService.ContainsData(ClipboardKey))
         {
             return;
         }
@@ -1579,7 +1602,7 @@ internal void Paste_Executed()
         {
             using var change = OperationsManager.BeginComplexChange();
 
-            var itemData = (InfoOwnerData)Clipboard.GetData(ClipboardKey);
+            var itemData = (InfoOwnerData)_clipboardService.GetData(ClipboardKey);
             var level = itemData.ItemLevel;
 
             if (level == InfoOwnerData.Level.Round)
@@ -2607,7 +2630,7 @@ private void ClearTempFolder()
         }
     }
 
-    internal async void SaveAs_Executed() => await SaveAsAsync();
+    internal Task SaveAs_Executed(object? arg) => SaveAsAsync();
 
     private async Task SaveAsAsync()
     {
@@ -3258,7 +3281,7 @@ private void SelectThemes_Executed(object? arg)
         var selectThemesViewModel = new SelectThemesViewModel(
             this,
             PlatformManager.Instance.ServiceProvider.GetRequiredService<IOptions<AppOptions>>().Value,
-            _loggerFactory);
+            PlatformManager.Instance.ServiceProvider.GetRequiredService<IDocumentViewModelFactory>());
 
         selectThemesViewModel.NewItem += OnNewItem;
         Dialog = selectThemesViewModel;
diff --git a/src/SIQuester/SIQuester.ViewModel/Workspaces/SettingsViewModel.cs b/src/SIQuester/SIQuester.ViewModel/Workspaces/SettingsViewModel.cs
index 2b823327..a427a81e 100644
--- a/src/SIQuester/SIQuester.ViewModel/Workspaces/SettingsViewModel.cs
+++ b/src/SIQuester/SIQuester.ViewModel/Workspaces/SettingsViewModel.cs
@@ -1,4 +1,5 @@
 using SIQuester.Model;
+using SIQuester.ViewModel.PlatformSpecific;
 using SIQuester.ViewModel.Properties;
 using System.Windows.Input;
 using Utils.Commands;
@@ -14,7 +15,7 @@ public sealed class SettingsViewModel : WorkspaceViewModel
 
     public override string Header => Resources.Options;
 
-    public string[] Fonts => System.Windows.Media.Fonts.SystemFontFamilies.Select(ff => ff.Source).OrderBy(f => f).ToArray();
+    public string[] Fonts => PlatformManager.Instance.FontFamilies;
 
     public bool SpellCheckingEnabled => Environment.OSVersion.Version > new Version(6, 2);
 
diff --git a/src/SIQuester/SIQuester/App.xaml.cs b/src/SIQuester/SIQuester/App.xaml.cs
index 8e005841..f2def116 100644
--- a/src/SIQuester/SIQuester/App.xaml.cs
+++ b/src/SIQuester/SIQuester/App.xaml.cs
@@ -8,6 +8,7 @@
 using NLog.Extensions.Logging;
 using NLog.Web;
 using SIPackages;
+using SIQuester.Helpers;
 using SIQuester.Model;
 using SIQuester.Services.Host;
 using SIQuester.ViewModel;
@@ -30,6 +31,7 @@
 using System.Runtime.InteropServices;
 using System.Text;
 using System.Windows;
+using System.Windows.Data;
 using System.Windows.Threading;
 using System.Xaml;
 #if !DEBUG
@@ -166,9 +168,14 @@ protected override void OnStartup(StartupEventArgs e)
             var siStorageClient = _host.Services.GetRequiredService<ISIStorageServiceClient>();
             var clipboardService = _host.Services.GetRequiredService<IClipboardService>();
             var options = _host.Services.GetRequiredService<IOptions<AppOptions>>();
+            var documentViewModelFactory = _host.Services.GetRequiredService<IDocumentViewModelFactory>();
             var loggerFactory = _host.Services.GetRequiredService<ILoggerFactory>();
 
-            _mainViewModel = new MainViewModel(e.Args, options.Value, siStorageClient, clipboardService, _host.Services, loggerFactory);
+            _mainViewModel = new MainViewModel(e.Args, options.Value, clipboardService, _host.Services, documentViewModelFactory, loggerFactory);
+            DocumentCollectionController.AttachTo(_mainViewModel);
+
+            var storageContextViewModel = _host.Services.GetRequiredService<StorageContextViewModel>();
+            storageContextViewModel.Load();
 
             MainWindow = new MainWindow { DataContext = _mainViewModel };
             MainWindow.Show();
@@ -197,11 +204,13 @@ private void ConfigureServices(HostBuilderContext ctx, IServiceCollection servic
         services.AddAppServiceClient(ctx.Configuration);
         services.AddSIStorageServiceClient(ctx.Configuration);
         services.AddChgkServiceClient(ctx.Configuration);
+
         services.AddSingleton(AppSettings.Default);
-        services.AddSingleton<IPackageTemplatesRepository, PackageTemplatesRepository>();
-        services.AddSingleton<IClipboardService, ClipboardService>();
-        services.AddSingleton<StorageViewModel>();
         services.Configure<AppOptions>(ctx.Configuration.GetSection(AppOptions.ConfigurationSectionName));
+
+        services.AddSingleton<IClipboardService, ClipboardService>();
+
+        services.AddSIQuester();
     }
 
     /// <summary>
diff --git a/src/SIQuester/SIQuester/Behaviors/CommandBindingsManager.cs b/src/SIQuester/SIQuester/Behaviors/CommandBindingsManager.cs
deleted file mode 100644
index 2ec83f21..00000000
--- a/src/SIQuester/SIQuester/Behaviors/CommandBindingsManager.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Windows;
-using System.Windows.Input;
-
-namespace SIQuester.ViewModel;
-
-/// <summary>
-/// Класс, позволяющий передать привязки команд визуальному элементу
-/// </summary>
-public sealed class CommandBindingsManager : DependencyObject
-{
-    public static CommandBindingCollection GetRegisterCommandBindings(DependencyObject obj) =>
-        (CommandBindingCollection)obj.GetValue(RegisterCommandBindingsProperty);
-
-    public static void SetRegisterCommandBindings(DependencyObject obj, CommandBindingCollection value) =>
-        obj.SetValue(RegisterCommandBindingsProperty, value);
-
-    public static readonly DependencyProperty RegisterCommandBindingsProperty =
-        DependencyProperty.RegisterAttached(
-            "RegisterCommandBindings",
-            typeof(CommandBindingCollection),
-            typeof(CommandBindingsManager),
-            new UIPropertyMetadata(null, OnRegisterCommandBindingChanged));
-
-    private static void OnRegisterCommandBindingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
-    {
-        if (sender is not UIElement element)
-        {
-            return;
-        }
-
-        if (e.OldValue is CommandBindingCollection bindings)
-        {
-            foreach (CommandBinding item in bindings)
-            {
-                element.CommandBindings.Remove(item);
-            }
-        }
-
-        if (e.NewValue is CommandBindingCollection newBindings)
-        {
-            element.CommandBindings.AddRange(newBindings);
-        }
-    }
-}
diff --git a/src/SIQuester/SIQuester/Helpers/DocumentCollectionController.cs b/src/SIQuester/SIQuester/Helpers/DocumentCollectionController.cs
new file mode 100644
index 00000000..42498513
--- /dev/null
+++ b/src/SIQuester/SIQuester/Helpers/DocumentCollectionController.cs
@@ -0,0 +1,17 @@
+using SIQuester.ViewModel;
+using System.ComponentModel;
+using System.Windows.Data;
+
+namespace SIQuester.Helpers;
+
+/// <summary>
+/// Manages main view document collection.
+/// </summary>
+internal static class DocumentCollectionController
+{
+    internal static void AttachTo(MainViewModel mainViewModel)
+    {
+        var collectionView = CollectionViewSource.GetDefaultView(mainViewModel.DocList);
+        collectionView.CurrentChanged += (sender, e) => mainViewModel.ActiveDocument = ((ICollectionView?)sender)?.CurrentItem as QDocument;
+    }
+}
diff --git a/src/SIQuester/SIQuester/Implementation/DesktopManager.cs b/src/SIQuester/SIQuester/Implementation/DesktopManager.cs
index 36178947..2f7c0853 100644
--- a/src/SIQuester/SIQuester/Implementation/DesktopManager.cs
+++ b/src/SIQuester/SIQuester/Implementation/DesktopManager.cs
@@ -32,6 +32,8 @@ internal sealed class DesktopManager : PlatformManager, IDisposable
 
     private const int MAX_PATH = 260;
 
+    public override string[] FontFamilies => System.Windows.Media.Fonts.SystemFontFamilies.Select(ff => ff.Source).OrderBy(f => f).ToArray();
+
     public override Tuple<int, int, int>? GetCurrentItemSelectionArea() =>
         ActionMenuViewModel.Instance.PlacementTarget is TextList box ? box.GetSelectionInfo() : null;
 
diff --git a/src/SIQuester/SIQuester/MainWindow.xaml b/src/SIQuester/SIQuester/MainWindow.xaml
index f0f42fb2..8c2aeb50 100644
--- a/src/SIQuester/SIQuester/MainWindow.xaml
+++ b/src/SIQuester/SIQuester/MainWindow.xaml
@@ -69,7 +69,7 @@
                 </DataTrigger>
             </DataTemplate.Triggers>
         </DataTemplate>
-        
+
         <Style x:Key="TabControlStyle1" TargetType="{x:Type TabControl}">
             <Setter Property="Padding" Value="0" />
             <Setter Property="HorizontalContentAlignment" Value="Center" />
@@ -79,17 +79,17 @@
             <Setter Property="BorderBrush" Value="#FFACACAC" />
             <Setter Property="BorderThickness" Value="0,1,0,0" />
             <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
-            
+
             <Setter Property="Template">
                 <Setter.Value>
                     <ControlTemplate TargetType="{x:Type TabControl}">
                         <Grid x:Name="templateRoot" ClipToBounds="True" SnapsToDevicePixels="True" KeyboardNavigation.TabNavigation="Local">
-                            
+
                             <Grid.ColumnDefinitions>
                                 <ColumnDefinition x:Name="ColumnDefinition0"/>
                                 <ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
                             </Grid.ColumnDefinitions>
-                            
+
                             <Grid.RowDefinitions>
                                 <RowDefinition x:Name="RowDefinition0" Height="Auto"/>
                                 <RowDefinition x:Name="RowDefinition1" Height="*"/>
@@ -126,7 +126,7 @@
                                     SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                             </Border>
                         </Grid>
-                        
+
                         <ControlTemplate.Triggers>
                             <Trigger Property="TabStripPlacement" Value="Bottom">
                                 <Setter Property="Grid.Row" TargetName="HeaderPanel" Value="1"/>
@@ -135,7 +135,7 @@
                                 <Setter Property="Height" TargetName="RowDefinition1" Value="Auto"/>
                                 <Setter Property="Margin" TargetName="HeaderPanel" Value="2,0,2,2"/>
                             </Trigger>
-                            
+
                             <Trigger Property="TabStripPlacement" Value="Left">
                                 <Setter Property="Grid.Row" TargetName="HeaderPanel" Value="0"/>
                                 <Setter Property="Grid.Row" TargetName="ContentPanel" Value="0"/>
@@ -147,7 +147,7 @@
                                 <Setter Property="Height" TargetName="RowDefinition1" Value="0"/>
                                 <Setter Property="Margin" TargetName="HeaderPanel" Value="2,2,0,2"/>
                             </Trigger>
-                            
+
                             <Trigger Property="TabStripPlacement" Value="Right">
                                 <Setter Property="Grid.Row" TargetName="HeaderPanel" Value="0"/>
                                 <Setter Property="Grid.Row" TargetName="ContentPanel" Value="0"/>
@@ -159,7 +159,7 @@
                                 <Setter Property="Height" TargetName="RowDefinition1" Value="0"/>
                                 <Setter Property="Margin" TargetName="HeaderPanel" Value="0,2,2,2"/>
                             </Trigger>
-                            
+
                             <Trigger Property="IsEnabled" Value="False">
                                 <Setter
                                     Property="TextElement.Foreground"
@@ -170,11 +170,11 @@
                     </ControlTemplate>
                 </Setter.Value>
             </Setter>
-            
+
             <Style.Triggers>
                 <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},Path=Items.Count}" Value="0">
                     <Setter Property="BorderThickness" Value="0" />
-                    
+
                     <Setter Property="Template">
                         <Setter.Value>
                             <ControlTemplate TargetType="TabControl">
@@ -184,14 +184,14 @@
                                         DataContext="{Binding Settings.History.Files}"
                                         HorizontalAlignment="Center"
                                         VerticalAlignment="Center">
-                                        
+
                                         <Grid.RowDefinitions>
                                             <RowDefinition Height="Auto" />
                                             <RowDefinition Height="Auto" />
                                         </Grid.RowDefinitions>
-                                        
+
                                         <TextBlock FontSize="24" HorizontalAlignment="Center" Text="{x:Static lp:Resources.RecentFiles}" />
-                                        
+
                                         <ItemsControl Grid.Row="1" ItemsSource="{Binding}" Margin="0,20,0,0">
                                             <ItemsControl.ItemTemplate>
                                                 <DataTemplate>
@@ -219,7 +219,7 @@
                                                                 Height="20">
                                                                 <Path Data="M0,0L1,1M0,1L1,0" Stroke="Gray" Stretch="Fill" Margin="3" />
                                                             </Button>
-                                                            
+
                                                             <TextBlock TextTrimming="CharacterEllipsis" Foreground="#FF040629" TextAlignment="Left">
                                                                 <Run Text="{Binding Mode=OneWay, Converter={StaticResource FileNameConverter}}" FontSize="18" />
                                                                 <LineBreak />
@@ -227,7 +227,7 @@
                                                             </TextBlock>
                                                         </DockPanel>
                                                     </Button>
-                                                    
+
                                                     <DataTemplate.Triggers>
                                                         <Trigger SourceName="open" Property="IsMouseOver" Value="True">
                                                             <Setter TargetName="remove" Property="Visibility" Value="Visible" />
@@ -255,14 +255,17 @@
             <Setter Property="Margin" Value="0" />
         </Style>
     </Window.Resources>
-    
-    <vm1:CommandBindingsManager.RegisterCommandBindings>
-        <MultiBinding Converter="{StaticResource UnionConverter}">
-            <Binding Path="CommandBindings" />
-            <Binding Path="ActiveDocument.CommandBindings" />
-        </MultiBinding>
-    </vm1:CommandBindingsManager.RegisterCommandBindings>
-    
+
+    <Window.CommandBindings>
+        <CommandBinding Command="ApplicationCommands.New" Executed="New_Executed" />
+        <CommandBinding Command="ApplicationCommands.Open" Executed="Open_Executed" />
+        <CommandBinding Command="ApplicationCommands.Help" Executed="Help_Executed" />
+        <CommandBinding Command="ApplicationCommands.Close" Executed="Close_Executed" />
+        <CommandBinding Command="ApplicationCommands.SaveAs" Executed="SaveAs_Executed" />
+        <CommandBinding Command="ApplicationCommands.Copy" Executed="Copy_Executed" />
+        <CommandBinding Command="ApplicationCommands.Paste" Executed="Paste_Executed" />
+    </Window.CommandBindings>
+
     <Window.InputBindings>
         <KeyBinding Gesture="CTRL+N" Command="ApplicationCommands.New" />
         <KeyBinding Gesture="CTRL+S" Command="{Binding ActiveDocument.Save}" />
diff --git a/src/SIQuester/SIQuester/MainWindow.xaml.cs b/src/SIQuester/SIQuester/MainWindow.xaml.cs
index 89005550..4b88e068 100644
--- a/src/SIQuester/SIQuester/MainWindow.xaml.cs
+++ b/src/SIQuester/SIQuester/MainWindow.xaml.cs
@@ -60,6 +60,11 @@ private void DocList_CollectionChanged(object? sender, NotifyCollectionChangedEv
                 };
 
                 tabControl1.Items.Add(tabItem);
+
+                if (sender != null)
+                {
+                    CollectionViewSource.GetDefaultView(sender).MoveCurrentToLast();
+                }
                 break;
 
             case NotifyCollectionChangedAction.Remove:
@@ -358,4 +363,18 @@ public CustomWindowAutomationPeer(FrameworkElement owner) : base(owner) { }
 
         protected override List<AutomationPeer> GetChildrenCore() => new();
     }
+
+    private void New_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).New.Execute(null);
+
+    private void Open_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel) DataContext).Open.Execute(e.Parameter);
+
+    private void Help_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).Help.Execute(null);
+
+    private void Close_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).Close.ExecuteAsync(null);
+
+    private void SaveAs_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).ActiveDocument?.SaveAs.ExecuteAsync(null);
+
+    private void Copy_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).ActiveDocument?.Copy.Execute(null);
+
+    private void Paste_Executed(object sender, ExecutedRoutedEventArgs e) => ((MainViewModel)DataContext).ActiveDocument?.Paste.Execute(null);
 }
diff --git a/src/SIQuester/SIQuester/Properties/Resources.Designer.cs b/src/SIQuester/SIQuester/Properties/Resources.Designer.cs
index 58c25b58..d7ff3d93 100644
--- a/src/SIQuester/SIQuester/Properties/Resources.Designer.cs
+++ b/src/SIQuester/SIQuester/Properties/Resources.Designer.cs
@@ -906,6 +906,15 @@ public static string Copy {
             }
         }
         
+        /// <summary>
+        ///   Ищет локализованную строку, похожую на Копировать шаблон.
+        /// </summary>
+        public static string CopyTemplate {
+            get {
+                return ResourceManager.GetString("CopyTemplate", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Ищет локализованную строку, похожую на Страна.
         /// </summary>
@@ -960,6 +969,15 @@ public static string Cut {
             }
         }
         
+        /// <summary>
+        ///   Ищет локализованную строку, похожую на Вырезать шаблон.
+        /// </summary>
+        public static string CutTemplate {
+            get {
+                return ResourceManager.GetString("CutTemplate", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Ищет локализованную строку, похожую на Удалить.
         /// </summary>
@@ -1789,6 +1807,15 @@ public static string OpenRecent {
             }
         }
         
+        /// <summary>
+        ///   Ищет локализованную строку, похожую на Необязательный фрагмент.
+        /// </summary>
+        public static string OptionalFragment {
+            get {
+                return ResourceManager.GetString("OptionalFragment", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Ищет локализованную строку, похожую на или.
         /// </summary>
@@ -1870,6 +1897,15 @@ public static string Paste {
             }
         }
         
+        /// <summary>
+        ///   Ищет локализованную строку, похожую на Вставить шаблон.
+        /// </summary>
+        public static string PasteTemplate {
+            get {
+                return ResourceManager.GetString("PasteTemplate", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Ищет локализованную строку, похожую на Запустить вопрос.
         /// </summary>
diff --git a/src/SIQuester/SIQuester/Properties/Resources.en-US.resx b/src/SIQuester/SIQuester/Properties/Resources.en-US.resx
index 46cfdd5e..53e24aa9 100644
--- a/src/SIQuester/SIQuester/Properties/Resources.en-US.resx
+++ b/src/SIQuester/SIQuester/Properties/Resources.en-US.resx
@@ -399,6 +399,9 @@
   <data name="Copy" xml:space="preserve">
     <value>Copy</value>
   </data>
+  <data name="CopyTemplate" xml:space="preserve">
+    <value>Copy template</value>
+  </data>
   <data name="Country" xml:space="preserve">
     <value>Country</value>
   </data>
@@ -417,6 +420,9 @@
   <data name="Cut" xml:space="preserve">
     <value>Cut</value>
   </data>
+  <data name="CutTemplate" xml:space="preserve">
+    <value>Cut template</value>
+  </data>
   <data name="Delete" xml:space="preserve">
     <value>Delete</value>
   </data>
@@ -690,6 +696,9 @@
   <data name="OpenRecent" xml:space="preserve">
     <value>Open recent</value>
   </data>
+  <data name="OptionalFragment" xml:space="preserve">
+    <value>Optional fragment</value>
+  </data>
   <data name="Or" xml:space="preserve">
     <value>or</value>
   </data>
@@ -717,6 +726,9 @@
   <data name="Paste" xml:space="preserve">
     <value>Insert</value>
   </data>
+  <data name="PasteTemplate" xml:space="preserve">
+    <value>Paste template</value>
+  </data>
   <data name="PlayQuestion" xml:space="preserve">
     <value>Start question</value>
   </data>
diff --git a/src/SIQuester/SIQuester/Properties/Resources.resx b/src/SIQuester/SIQuester/Properties/Resources.resx
index 4e1ae442..ac69cc6b 100644
--- a/src/SIQuester/SIQuester/Properties/Resources.resx
+++ b/src/SIQuester/SIQuester/Properties/Resources.resx
@@ -399,6 +399,9 @@
   <data name="Copy" xml:space="preserve">
     <value>Копировать</value>
   </data>
+  <data name="CopyTemplate" xml:space="preserve">
+    <value>Копировать шаблон</value>
+  </data>
   <data name="Country" xml:space="preserve">
     <value>Страна</value>
   </data>
@@ -417,6 +420,9 @@
   <data name="Cut" xml:space="preserve">
     <value>Вырезать</value>
   </data>
+  <data name="CutTemplate" xml:space="preserve">
+    <value>Вырезать шаблон</value>
+  </data>
   <data name="Delete" xml:space="preserve">
     <value>Удалить</value>
   </data>
@@ -694,6 +700,9 @@
   <data name="OpenRecent" xml:space="preserve">
     <value>Открыть недавние</value>
   </data>
+  <data name="OptionalFragment" xml:space="preserve">
+    <value>Необязательный фрагмент</value>
+  </data>
   <data name="Or" xml:space="preserve">
     <value>или</value>
   </data>
@@ -721,6 +730,9 @@
   <data name="Paste" xml:space="preserve">
     <value>Вставить</value>
   </data>
+  <data name="PasteTemplate" xml:space="preserve">
+    <value>Вставить шаблон</value>
+  </data>
   <data name="PlayQuestion" xml:space="preserve">
     <value>Запустить вопрос</value>
   </data>
diff --git a/src/SIQuester/SIQuester/SIQuester.csproj b/src/SIQuester/SIQuester/SIQuester.csproj
index 05b42fbd..364e15bb 100644
--- a/src/SIQuester/SIQuester/SIQuester.csproj
+++ b/src/SIQuester/SIQuester/SIQuester.csproj
@@ -55,6 +55,7 @@
     <PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
     <PackageReference Include="NLog.Web.AspNetCore" Version="5.2.0" />
     <PackageReference Include="WindowsAPICodePackShell" Version="7.0.4" />
+	<PackageReference Include="MahApps.Metro" Version="2.4.10" />
   </ItemGroup>
   <ItemGroup>
     <None Include="licenses\MahApps.Metro.LICENSE">
diff --git a/src/SIQuester/SIQuester/Services/Host/ClipboardService.cs b/src/SIQuester/SIQuester/Services/Host/ClipboardService.cs
index 0f4a379e..c7e529d3 100644
--- a/src/SIQuester/SIQuester/Services/Host/ClipboardService.cs
+++ b/src/SIQuester/SIQuester/Services/Host/ClipboardService.cs
@@ -3,8 +3,11 @@
 
 namespace SIQuester.Services.Host;
 
+/// <inheritdoc />
 internal sealed class ClipboardService : IClipboardService
 {
+    public bool ContainsData(string format) => Clipboard.ContainsData(format);
+
     public object GetData(string format) => Clipboard.GetData(format);
 
     public void SetData(string format, object data) => Clipboard.SetData(format, data);
diff --git a/src/SIQuester/SIQuester/View/SpardEditorView.xaml b/src/SIQuester/SIQuester/View/SpardEditorView.xaml
index 954eff41..cdf2dacb 100644
--- a/src/SIQuester/SIQuester/View/SpardEditorView.xaml
+++ b/src/SIQuester/SIQuester/View/SpardEditorView.xaml
@@ -5,6 +5,9 @@
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+    xmlns:lp="clr-namespace:SIQuester.Properties"
+    xmlns:lvm="clr-namespace:SIQuester.ViewModel;assembly=SIQuester.ViewModel"
+    d:DataContext="{d:DesignInstance lvm:SpardTemplateViewModel}"
     mc:Ignorable="d"
     d:DesignHeight="300"
     d:DesignWidth="300">
@@ -15,8 +18,8 @@
                     <StackPanel Orientation="Vertical"/>
                 </ItemsPanelTemplate>
             </Menu.ItemsPanel>
-            
-            <MenuItem Header="&lt;&gt;" ItemsSource="{Binding Aliases}" ToolTip="Объект" FontSize="13" Height="25">
+
+            <MenuItem Header="&lt;&gt;" ItemsSource="{Binding Aliases}" ToolTip="{x:Static lp:Resources.Object}" FontSize="13" Height="25">
                 <MenuItem.ItemContainerStyle>
                     <Style TargetType="MenuItem">
                         <Setter Property="Command" Value="{Binding Path=DataContext.InsertAlias, RelativeSource={RelativeSource FindAncestor, AncestorType=Menu, AncestorLevel=1}}" />
@@ -30,24 +33,24 @@
                     </DataTemplate>
                 </MenuItem.ItemTemplate>
             </MenuItem>
-            
-            <MenuItem Header="?" Command="{Binding InsertOptional}" ToolTip="Необязательный фрагмент" Height="25">
+
+            <MenuItem Header="?" Command="{Binding InsertOptional}" ToolTip="{x:Static lp:Resources.OptionalFragment}" Height="25">
                 <MenuItem.HeaderTemplate>
                     <DataTemplate>
                         <TextBlock Text="{Binding}" FontSize="18" Margin="5,0,0,0" HorizontalAlignment="Stretch" />
                     </DataTemplate>
                 </MenuItem.HeaderTemplate>
             </MenuItem>
-            
-            <MenuItem Command="{Binding Cut}" ToolTip="Вырезать шаблон" Height="25">
+
+            <MenuItem Command="{Binding Cut}" ToolTip="{x:Static lp:Resources.CutTemplate}" Height="25">
                 <MenuItem.HeaderTemplate>
                     <DataTemplate>
                         <Path Stretch="Uniform" Fill="#FF555555" Width="18" Margin="2" Data="{Binding Source={StaticResource app_cut},Path=Data}" />
                     </DataTemplate>
                 </MenuItem.HeaderTemplate>
             </MenuItem>
-            
-            <MenuItem Command="{Binding Copy}" ToolTip="Копировать шаблон" Height="25">
+
+            <MenuItem Command="{Binding Copy}" ToolTip="{x:Static lp:Resources.CopyTemplate}" Height="25">
                 <MenuItem.HeaderTemplate>
                     <DataTemplate>
                         <Path
@@ -59,8 +62,8 @@
                     </DataTemplate>
                 </MenuItem.HeaderTemplate>
             </MenuItem>
-            
-            <MenuItem Command="{Binding Paste}" ToolTip="Вставить шаблон" Height="25">
+
+            <MenuItem Command="{Binding Paste}" ToolTip="{x:Static lp:Resources.PasteTemplate}" Height="25">
                 <MenuItem.HeaderTemplate>
                     <DataTemplate>
                         <Path
diff --git a/src/SIQuester/SIQuester/appsettings.json b/src/SIQuester/SIQuester/appsettings.json
index 1aa90e24..c146bf6f 100644
--- a/src/SIQuester/SIQuester/appsettings.json
+++ b/src/SIQuester/SIQuester/appsettings.json
@@ -6,7 +6,7 @@
     "ServiceUri": "https://vladimirkhil.com/sistorage/"
   },
   "SIQuester": {
-    "UpgradeNewPackages": false,
+    "UpgradeNewPackages": true,
     "UpgradeOpenedPackages": false,
     "SelectAnswerTypeEnabled": false
   },