using MainShell.Common; using MainShell.Models; using MainShell.Recipe.Models; using Newtonsoft.Json.Linq; using Stylet; using System; using System.Collections.ObjectModel; using System.IO; using System.Linq; namespace MainShell.Recipe.ViewModel { public abstract class RecipeViewModelBase : BaseScreen, IRecipeViewModel where TRecipe : RecipeBase, new() { private string _lastSavedSnapshot = string.Empty; protected string LastSavedSnapshot => _lastSavedSnapshot; private bool _skipLeaveCheck; private RecipeWrap _selectedRecipeWrap; public ObservableCollection RecipeWraps { get; protected set; } = new ObservableCollection(); public RecipeWrap SelectedRecipeWrap { get { return _selectedRecipeWrap; } set { if (_selectedRecipeWrap == value) return; if (!_skipLeaveCheck && _selectedRecipeWrap != null && value != null && !TryLeaveCurrentRecipeContext()) { NotifyOfPropertyChange(() => SelectedRecipeWrap); return; } if (SetAndNotify(ref _selectedRecipeWrap, value)) { LoadRecipeFromWrap(value); } } } public RecipeButtonGroupViewModel RecipeButtonGroupViewModel { get; protected set; } public virtual bool CanCopyRecipe => true; public virtual bool CanImportRecipe => false; public virtual bool CanExportRecipe => false; public bool HasUnsavedChanges => IsRecipeDirty(); public string CurrentRecipeDisplayName => CurrentRecipe != null ? CurrentRecipe.RecipeName : EmptyRecipeDisplayName; public RecipeManager RecipeManager { get; } protected RecipeViewModelBase(RecipeManager recipeManager) { RecipeManager = recipeManager; } protected abstract TRecipe CurrentRecipe { get; set; } protected abstract string RecipeFolderPath { get; } protected abstract string EmptyRecipeDisplayName { get; } protected abstract string GetActiveRecipeName(); protected abstract void SwitchActiveRecipe(string recipeName); protected abstract void ClearActiveRecipe(); protected virtual void OnRecipeLoaded(TRecipe recipe, RecipeWrap recipeWrap) { } protected virtual string GetUnsavedChangesMessage() => $"配方“{CurrentRecipeDisplayName}”有未保存修改,是否先保存?"; protected virtual void AfterDeleteCurrentRecipe(string deletedRecipeName) { } protected virtual void AfterRecipeRenamed(string oldRecipeName, string newRecipeName) { } public virtual bool TryLeaveCurrentRecipeContext() { if (!HasUnsavedChanges) return true; var result = MwMessageBox.Show(GetUnsavedChangesMessage(), "未保存提示", System.Windows.MessageBoxButton.YesNoCancel, System.Windows.MessageBoxImage.Warning); if (result == System.Windows.MessageBoxResult.Cancel) return false; if (result == System.Windows.MessageBoxResult.Yes) SaveCurrentRecipe(false); return true; } public void SaveCurrentRecipeSilently() { SaveCurrentRecipe(false); } protected void RegisterButtonGroupEvents() { RecipeButtonGroupViewModel = new RecipeButtonGroupViewModel(); RecipeButtonGroupViewModel.CreateNewRequested -= OnCreateNewRecipe; RecipeButtonGroupViewModel.ApplyRequested -= OnApplyRecipe; RecipeButtonGroupViewModel.DeleteRequested -= OnDeleteRecipe; RecipeButtonGroupViewModel.CopyRequested -= OnCopyRecipe; RecipeButtonGroupViewModel.ImportRequested -= OnImportRecipe; RecipeButtonGroupViewModel.SaveRequested -= OnSaveRecipe; RecipeButtonGroupViewModel.ExportRequested -= OnExportRecipe; RecipeButtonGroupViewModel.ReNameRequested -= OnReNameRecipe; RecipeButtonGroupViewModel.ClearRequested -= OnClearRecipe; RecipeButtonGroupViewModel.CreateNewRequested += OnCreateNewRecipe; RecipeButtonGroupViewModel.ApplyRequested += OnApplyRecipe; RecipeButtonGroupViewModel.DeleteRequested += OnDeleteRecipe; RecipeButtonGroupViewModel.CopyRequested += OnCopyRecipe; RecipeButtonGroupViewModel.ImportRequested += OnImportRecipe; RecipeButtonGroupViewModel.SaveRequested += OnSaveRecipe; RecipeButtonGroupViewModel.ExportRequested += OnExportRecipe; RecipeButtonGroupViewModel.ReNameRequested += OnReNameRecipe; RecipeButtonGroupViewModel.ClearRequested += OnClearRecipe; } public virtual void OnCreateNewRecipe(object sender, EventArgs eventArgs) { if (eventArgs is RecipeEventArgs args) { if (!TryLeaveCurrentRecipeContext()) return; var recipeWrap = new RecipeWrap { RecipeName = args.RecipeName, CreateTime = DateTime.UtcNow, ModifiedTime = DateTime.UtcNow, }; RecipeWraps.Add(recipeWrap); SelectRecipeWrapInternal(recipeWrap); } } public virtual void OnDeleteRecipe(object sender, EventArgs eventArgs) { if (SelectedRecipeWrap == null) return; if (!TryLeaveCurrentRecipeContext()) return; var deletedRecipeName = SelectedRecipeWrap.RecipeName; if (!RecipeManager.DeleteRecipeFolder(RecipeFolderPath, deletedRecipeName, out var error)) { MwMessageBox.Show(error); return; } RecipeWraps.Remove(SelectedRecipeWrap); AfterDeleteCurrentRecipe(deletedRecipeName); if (RecipeWraps.Count > 0) { SelectRecipeWrapInternal(RecipeWraps[0]); } else { SelectedRecipeWrap = null; CurrentRecipe = null; ClearActiveRecipe(); CaptureSavedSnapshot(); } } public virtual void OnCopyRecipe(object sender, EventArgs eventArgs) { if (!(eventArgs is RecipeEventArgs args) || SelectedRecipeWrap == null) return; var sourceRecipeName = SelectedRecipeWrap.RecipeName; if (string.IsNullOrWhiteSpace(sourceRecipeName) || string.IsNullOrWhiteSpace(args.RecipeName)) return; if (CurrentRecipe != null && string.Equals(CurrentRecipe.RecipeName, sourceRecipeName, StringComparison.Ordinal) && HasUnsavedChanges) { SaveCurrentRecipe(false); } if (!RecipeManager.CopyRecipeFolder(RecipeFolderPath, sourceRecipeName, args.RecipeName, out var error)) { MwMessageBox.Show(error); return; } var copiedRecipeWrap = new RecipeWrap { RecipeName = args.RecipeName, CreateTime = DateTime.UtcNow, ModifiedTime = DateTime.UtcNow, }; RecipeWraps.Add(copiedRecipeWrap); SelectRecipeWrapInternal(copiedRecipeWrap); } public virtual void OnApplyRecipe(object sender, EventArgs eventArgs) { if (eventArgs is RecipeEventArgs args) { var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.RecipeName); if (recipeWrap == null) return; if (!ReferenceEquals(SelectedRecipeWrap, recipeWrap)) { if (!TryLeaveCurrentRecipeContext()) return; SelectRecipeWrapInternal(recipeWrap); } if (SelectedRecipeWrap == null) return; if (CurrentRecipe != null && IsRecipeDirty()) { SaveCurrentRecipe(false); } SwitchActiveRecipe(SelectedRecipeWrap.RecipeName); UpdateInUseState(SelectedRecipeWrap); } } public abstract void OnImportRecipe(object sender, EventArgs eventArgs); public virtual void OnSaveRecipe(object sender, EventArgs eventArgs) { CommonUti.RunOnUi(() => { if (eventArgs is RecipeEventArgs) { SaveCurrentRecipe(true); } }); } public abstract void OnExportRecipe(object sender, EventArgs eventArgs); public abstract void OnReNameRecipe(object sender, EventArgs eventArgs); public virtual void OnClearRecipe(object sender, EventArgs eventArgs) { if (!TryLeaveCurrentRecipeContext()) return; if (SelectedRecipeWrap == null) return; var clearedRecipe = new TRecipe { RecipeName = SelectedRecipeWrap.RecipeName }; CurrentRecipe = clearedRecipe; OnRecipeLoaded(clearedRecipe, SelectedRecipeWrap); NotifyOfPropertyChange(() => HasUnsavedChanges); } protected bool IsRecipeDirty() { if (CurrentRecipe == null) return false; var path = Path.Combine(CurrentRecipe.Dir ?? string.Empty, CurrentRecipe.FileName ?? string.Empty); if (!File.Exists(path)) return true; return !AreSnapshotsEquivalent(_lastSavedSnapshot, CurrentRecipe.CreateSnapshot()); } protected void CaptureSavedSnapshot() { _lastSavedSnapshot = CurrentRecipe != null ? CurrentRecipe.CreateSnapshot() : string.Empty; NotifyOfPropertyChange(() => HasUnsavedChanges); NotifyOfPropertyChange(() => CurrentRecipeDisplayName); } protected bool TryRenameRecipe(RecipeRenameEventArgs args) { if (args == null) return false; var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.OldRecipeName); if (recipeWrap == null) return false; if (!RecipeManager.RenameFolder(RecipeFolderPath, args.OldRecipeName, args.NewRecipeName, out var error)) { MwMessageBox.Show(error); return false; } recipeWrap.RecipeName = args.NewRecipeName; recipeWrap.ModifiedTime = DateTime.UtcNow; if (CurrentRecipe != null && string.Equals(CurrentRecipe.RecipeName, args.OldRecipeName, StringComparison.Ordinal)) { CurrentRecipe.RecipeName = args.NewRecipeName; } if (ReferenceEquals(recipeWrap, SelectedRecipeWrap)) { SwitchActiveRecipe(args.NewRecipeName); } CaptureSavedSnapshot(); AfterRecipeRenamed(args.OldRecipeName, args.NewRecipeName); return true; } protected void SaveCurrentRecipe(bool showSuccessMessage) { if (CurrentRecipe == null) return; CurrentRecipe.Write(); CaptureSavedSnapshot(); if (SelectedRecipeWrap != null) { SelectedRecipeWrap.ModifiedTime = DateTime.UtcNow; } if (showSuccessMessage) { MwMessageBox.Show("保存成功"); } } private static bool AreSnapshotsEquivalent(string leftSnapshot, string rightSnapshot) { var left = string.IsNullOrWhiteSpace(leftSnapshot) ? null : ParseSnapshotToken(leftSnapshot); var right = string.IsNullOrWhiteSpace(rightSnapshot) ? null : ParseSnapshotToken(rightSnapshot); if (left == null || right == null) return false; return JToken.DeepEquals(left, right); } private static JToken ParseSnapshotToken(string snapshot) { try { return JToken.Parse(snapshot); } catch { return null; } } protected void SelectRecipeWrapInternal(RecipeWrap recipeWrap) { try { _skipLeaveCheck = true; SelectedRecipeWrap = recipeWrap; } finally { _skipLeaveCheck = false; } } protected void InitializeSelection(string activeRecipeName) { SyncSelection(activeRecipeName, true); } protected virtual void UpdateInUseState(RecipeWrap activeRecipeWrap) { foreach (var wrap in RecipeWraps) { wrap.IsInUse = ReferenceEquals(wrap, activeRecipeWrap); } } public void SyncSelectionFromActiveRecipe() { SyncSelection(GetActiveRecipeName(), false); } private void SyncSelection(string recipeName, bool fallbackToFirst) { if (RecipeWraps == null || RecipeWraps.Count == 0) { UpdateInUseState(null); return; } var target = string.IsNullOrWhiteSpace(recipeName) ? null : RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, recipeName, StringComparison.Ordinal)); if (target == null) { UpdateInUseState(null); if (!fallbackToFirst) return; target = RecipeWraps[0]; } if (ReferenceEquals(SelectedRecipeWrap, target)) { UpdateInUseState(target); return; } SelectRecipeWrapInternal(target); } private void LoadRecipeFromWrap(RecipeWrap recipeWrap) { if (recipeWrap == null) { UpdateInUseStateByActiveRecipe(); return; } var newRecipe = new TRecipe { RecipeName = recipeWrap.RecipeName }; newRecipe.Read(); CurrentRecipe = newRecipe; CaptureSavedSnapshot(); UpdateInUseStateByActiveRecipe(); OnRecipeLoaded(newRecipe, recipeWrap); } private void UpdateInUseStateByActiveRecipe() { var activeRecipeName = GetActiveRecipeName(); var activeRecipeWrap = RecipeWraps == null || string.IsNullOrWhiteSpace(activeRecipeName) ? null : RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, activeRecipeName, StringComparison.Ordinal)); UpdateInUseState(activeRecipeWrap); } } }