From d5bc36ba87f56e5c68dc7436897d81f9125314b3 Mon Sep 17 00:00:00 2001 From: Bogdan Zavu Date: Fri, 11 Mar 2022 10:18:41 -0500 Subject: [PATCH] DYN-4034 : open file from json content (#12671) * open file from json content * adress feedback 1 * openfilefromjson command and test * adress feedback Co-authored-by: Bzz --- src/DynamoCore/Models/DynamoModel.cs | 24 +++- src/DynamoCore/Models/DynamoModelCommands.cs | 7 + src/DynamoCore/Models/RecordableCommands.cs | 122 ++++++++++++++--- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.en-US.resx | 3 + src/DynamoCore/Properties/Resources.resx | 3 + .../Scheduler/DynamoSchedulerInternals.cs | 1 + src/DynamoCoreWpf/Commands/DynamoCommands.cs | 2 + .../ViewModels/Core/DynamoViewModel.cs | 123 ++++++++++++++++-- .../Core/DynamoViewModelDelegateCommands.cs | 4 + src/DynamoUtilities/DebugModes.cs | 2 + src/DynamoUtilities/PathHelper.cs | 45 +++++-- .../Models/OpenFileFromJsonCommandTest.cs | 43 ++++++ 13 files changed, 347 insertions(+), 41 deletions(-) create mode 100644 test/DynamoCoreTests/Models/OpenFileFromJsonCommandTest.cs diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs index b3be53d1492..b52bb4fea84 100644 --- a/src/DynamoCore/Models/DynamoModel.cs +++ b/src/DynamoCore/Models/DynamoModel.cs @@ -1647,6 +1647,18 @@ public void ForceRun() #region save/load + /// + /// Opens a Dynamo workspace from a Json string. + /// + /// Json file content + /// Set this to true to discard + /// execution mode specified in the file and set manual mode + public void OpenFileFromJson(string fileContents, bool forceManualExecutionMode = false) + { + OpenJsonFileFromPath(fileContents, "", forceManualExecutionMode); + return; + } + /// /// Opens a Dynamo workspace from a path to a file on disk. /// @@ -1852,7 +1864,10 @@ private bool OpenJsonFile( bool forceManualExecutionMode, out WorkspaceModel workspace) { - CustomNodeManager.AddUninitializedCustomNodesInPath(Path.GetDirectoryName(filePath), IsTestMode); + if (!string.IsNullOrEmpty(filePath)) + { + CustomNodeManager.AddUninitializedCustomNodesInPath(Path.GetDirectoryName(filePath), IsTestMode); + } var currentHomeSpace = Workspaces.OfType().FirstOrDefault(); currentHomeSpace.UndefineCBNFunctionDefinitions(); @@ -1870,7 +1885,7 @@ private bool OpenJsonFile( CustomNodeManager, this.LinterManager); - workspace.FileName = filePath; + workspace.FileName = string.IsNullOrEmpty(filePath) ? "" : filePath; workspace.ScaleFactor = dynamoPreferences.ScaleFactor; // NOTE: This is to handle the case of opening a JSON file that does not have a version string @@ -1887,6 +1902,11 @@ private bool OpenJsonFile( homeWorkspace.ReCompileCodeBlockNodesForFunctionDefinitions(); + if (string.IsNullOrEmpty(workspace.FileName)) + { + workspace.HasUnsavedChanges = true; + } + RunType runType; if (!homeWorkspace.HasRunWithoutCrash || !Enum.TryParse(dynamoPreferences.RunType, false, out runType) || forceManualExecutionMode) runType = RunType.Manual; diff --git a/src/DynamoCore/Models/DynamoModelCommands.cs b/src/DynamoCore/Models/DynamoModelCommands.cs index ee1ee7d76d5..b8a26d1949a 100644 --- a/src/DynamoCore/Models/DynamoModelCommands.cs +++ b/src/DynamoCore/Models/DynamoModelCommands.cs @@ -52,6 +52,13 @@ protected virtual void OpenFileImpl(OpenFileCommand command) //ClipBoard.Clear(); } + protected virtual void OpenFileFromJsonImpl(OpenFileFromJsonCommand command) + { + string fileContents = command.FileContents; + bool forceManualMode = command.ForceManualExecutionMode; + OpenFileFromJson(fileContents, forceManualMode); + } + private void RunCancelImpl(RunCancelCommand command) { var model = CurrentWorkspace as HomeWorkspaceModel; diff --git a/src/DynamoCore/Models/RecordableCommands.cs b/src/DynamoCore/Models/RecordableCommands.cs index 902da3ce26f..2847860a9f9 100644 --- a/src/DynamoCore/Models/RecordableCommands.cs +++ b/src/DynamoCore/Models/RecordableCommands.cs @@ -141,67 +141,70 @@ internal static RecordableCommand Deserialize(XmlElement element) switch (element.Name) { - case "OpenFileCommand": + case nameof(OpenFileCommand): command = OpenFileCommand.DeserializeCore(element); break; - case "PausePlaybackCommand": + case nameof(OpenFileFromJsonCommand): + command = OpenFileFromJsonCommand.DeserializeCore(element); + break; + case nameof(PausePlaybackCommand): command = PausePlaybackCommand.DeserializeCore(element); break; - case "RunCancelCommand": + case nameof(RunCancelCommand): command = RunCancelCommand.DeserializeCore(element); break; - case "CreateNodeCommand": + case nameof(CreateNodeCommand): command = CreateNodeCommand.DeserializeCore(element); break; - case "SelectModelCommand": + case nameof(SelectModelCommand): command = SelectModelCommand.DeserializeCore(element); break; - case "CreateNoteCommand": + case nameof(CreateNoteCommand): command = CreateNoteCommand.DeserializeCore(element); break; - case "SelectInRegionCommand": + case nameof(SelectInRegionCommand): command = SelectInRegionCommand.DeserializeCore(element); break; - case "DragSelectionCommand": + case nameof(DragSelectionCommand): command = DragSelectionCommand.DeserializeCore(element); break; - case "MakeConnectionCommand": + case nameof(MakeConnectionCommand): command = MakeConnectionCommand.DeserializeCore(element); break; - case "DeleteModelCommand": + case nameof(DeleteModelCommand): command = DeleteModelCommand.DeserializeCore(element); break; - case "UndoRedoCommand": + case nameof(UndoRedoCommand): command = UndoRedoCommand.DeserializeCore(element); break; - case "ModelEventCommand": + case nameof(ModelEventCommand): command = ModelEventCommand.DeserializeCore(element); break; - case "UpdateModelValueCommand": + case nameof(UpdateModelValueCommand): command = UpdateModelValueCommand.DeserializeCore(element); break; - case "ConvertNodesToCodeCommand": + case nameof(ConvertNodesToCodeCommand): command = ConvertNodesToCodeCommand.DeserializeCore(element); break; - case "CreateCustomNodeCommand": + case nameof(CreateCustomNodeCommand): command = CreateCustomNodeCommand.DeserializeCore(element); break; - case "SwitchTabCommand": + case nameof(SwitchTabCommand): command = SwitchTabCommand.DeserializeCore(element); break; - case "CreateAnnotationCommand": + case nameof(CreateAnnotationCommand): command = CreateAnnotationCommand.DeserializeCore(element); break; - case "UngroupModelCommand": + case nameof(UngroupModelCommand): command = UngroupModelCommand.DeserializeCore(element); break; - case "AddPresetCommand": + case nameof(AddPresetCommand): command = AddPresetCommand.DeserializeCore(element); break; - case "ApplyPresetCommand": + case nameof(ApplyPresetCommand): command = ApplyPresetCommand.DeserializeCore(element); break; - case "CreateAndConnectNodeCommand": + case nameof(CreateAndConnectNodeCommand): command = CreateAndConnectNodeCommand.DeserializeCore(element); break; } @@ -546,6 +549,83 @@ internal override void TrackAnalytics() #endregion } + /// + /// A command to open a file from Json content. + /// + [DataContract] + public class OpenFileFromJsonCommand : RecordableCommand + { + #region Public Class Methods + + /// + /// + /// + /// The Json content of a file. + /// Should the file be opened in manual execution mode? + public OpenFileFromJsonCommand(string fileContents, bool forceManualExecutionMode = false) + { + FileContents = fileContents; + ForceManualExecutionMode = forceManualExecutionMode; + } + + internal static OpenFileFromJsonCommand DeserializeCore(XmlElement element) + { + XmlElementHelper helper = new XmlElementHelper(element); + string xmlFileContents = helper.ReadString("XmlFileContents"); + return new OpenFileFromJsonCommand(xmlFileContents); + } + + #endregion + + #region Public Command Properties + + [DataMember] + internal string FileContents { get; private set; } + internal bool ForceManualExecutionMode { get; private set; } + private DynamoModel dynamoModel; + + #endregion + + #region Protected Overridable Methods + + protected override void ExecuteCore(DynamoModel dynamoModel) + { + this.dynamoModel = dynamoModel; + dynamoModel.OpenFileFromJsonImpl(this); + } + + + protected override void SerializeCore(XmlElement element) + { + var helper = new XmlElementHelper(element); + helper.SetAttribute("XmlFileContents", FileContents); + } + + + internal override void TrackAnalytics() + { + // Log file open action and the number of nodes in the opened workspace + Dynamo.Logging.Analytics.TrackFileOperationEvent( + "In memory json file", + Logging.Actions.Open, + dynamoModel.CurrentWorkspace.Nodes.Count()); + + // If there are unresolved nodes in the opened workspace, log the node names and count + var unresolvedNodes = dynamoModel.CurrentWorkspace.Nodes.OfType(); + if (unresolvedNodes != null && unresolvedNodes.Any()) + { + Dynamo.Logging.Analytics.TrackEvent( + Logging.Actions.Unresolved, + Logging.Categories.NodeOperations, + unresolvedNodes.Select(n => string.Format("{0}:{1}", n.LegacyAssembly, n.LegacyFullName)) + .Aggregate((x, y) => string.Format("{0}, {1}", x, y)), + unresolvedNodes.Count()); + } + } + + #endregion + } + /// /// A command used to execute or cancel execution. /// diff --git a/src/DynamoCore/Properties/Resources.Designer.cs b/src/DynamoCore/Properties/Resources.Designer.cs index e84aca21d97..7579f711f65 100644 --- a/src/DynamoCore/Properties/Resources.Designer.cs +++ b/src/DynamoCore/Properties/Resources.Designer.cs @@ -650,6 +650,15 @@ public static string FileCannotBeOpened { } } + /// + /// Looks up a localized string similar to File load failure. + /// + public static string FileLoadFailureMessageBoxTitle { + get { + return ResourceManager.GetString("FileLoadFailureMessageBoxTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your file may not open correctly. /// diff --git a/src/DynamoCore/Properties/Resources.en-US.resx b/src/DynamoCore/Properties/Resources.en-US.resx index 5722ef291ae..fdb795e729e 100644 --- a/src/DynamoCore/Properties/Resources.en-US.resx +++ b/src/DynamoCore/Properties/Resources.en-US.resx @@ -831,6 +831,9 @@ This package likely contains an assembly that is blocked. You will need to load Library load failure + + File load failure + Actions diff --git a/src/DynamoCore/Properties/Resources.resx b/src/DynamoCore/Properties/Resources.resx index 2635648b02d..4c78b5a67ef 100644 --- a/src/DynamoCore/Properties/Resources.resx +++ b/src/DynamoCore/Properties/Resources.resx @@ -834,6 +834,9 @@ This package likely contains an assembly that is blocked. You will need to load Library load failure + + File load failure + Actions diff --git a/src/DynamoCore/Scheduler/DynamoSchedulerInternals.cs b/src/DynamoCore/Scheduler/DynamoSchedulerInternals.cs index 6f41c100e13..057882b9a12 100644 --- a/src/DynamoCore/Scheduler/DynamoSchedulerInternals.cs +++ b/src/DynamoCore/Scheduler/DynamoSchedulerInternals.cs @@ -47,6 +47,7 @@ public bool HasPendingTasks } } } + #endregion #region Private Class Helper Methods diff --git a/src/DynamoCoreWpf/Commands/DynamoCommands.cs b/src/DynamoCoreWpf/Commands/DynamoCommands.cs index 5f4dd79da92..5ccb7240839 100644 --- a/src/DynamoCoreWpf/Commands/DynamoCommands.cs +++ b/src/DynamoCoreWpf/Commands/DynamoCommands.cs @@ -139,6 +139,7 @@ void OnModelCommandCompleted(DynamoModel.RecordableCommand command) case "CreateCustomNodeCommand": case "AddPresetCommand": case "ApplyPresetCommand": + case "OpenFileFromJsonCommand": // for this commands there is no need // to do anything after execution break; @@ -167,6 +168,7 @@ void OnModelCommandStarting(DynamoModel.RecordableCommand command) break; case "OpenFileCommand": + case "OpenFileFromJsonCommand": case "RunCancelCommand": case "ForceRunCancelCommand": case "CreateNodeCommand": diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 900f71574c3..c4747858a40 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -173,6 +173,7 @@ public void WorkspaceActualSize(double width, double height) private WorkspaceViewModel currentWorkspaceViewModel; private string filePath; + private string fileContents; /// /// The index in the collection of workspaces of the current workspace. /// This property is bound to the SelectedIndex property in the workspaces tab control @@ -1447,6 +1448,38 @@ public FileDialog GetSaveDialog(WorkspaceModel workspace) return fileDialog; } + /// + /// Attempts to open a file using the Json content passed to OpenFromJsonCommand, but wraps + /// the call with a check to make sure no unsaved changes to the HomeWorkspace are lost. + /// + /// + private void OpenFromJsonIfSaved(object openCommand) + { + filePath = string.Empty; + fileContents = string.Empty; + + var command = openCommand as DynamoModel.OpenFileFromJsonCommand; + if (command == null) + { + return; + } + + if (HomeSpace != null && HomeSpace.HasUnsavedChanges) + { + if (AskUserToSaveWorkspaceOrCancel(HomeSpace)) + { + fileContents = command.FileContents; + ExecuteCommand(command); + ShowStartPage = false; + } + } + else + { + OpenFromJsonCommand.Execute(new Tuple(command.FileContents, command.ForceManualExecutionMode)); + ShowStartPage = false; + } + } + /// /// Attempts to open a file using the passed open command, but wraps the call /// with a check to make sure no unsaved changes to the HomeWorkspace are lost. @@ -1454,6 +1487,9 @@ public FileDialog GetSaveDialog(WorkspaceModel workspace) /// private void OpenIfSaved(object openCommand) { + fileContents = string.Empty; + filePath = string.Empty; + var command = openCommand as DynamoModel.OpenFileCommand; if (command == null) { @@ -1464,16 +1500,74 @@ private void OpenIfSaved(object openCommand) { if (AskUserToSaveWorkspaceOrCancel(HomeSpace)) { - this.filePath = command.FilePath; - this.ExecuteCommand(command); - this.ShowStartPage = false; + filePath = command.FilePath; + ExecuteCommand(command); + ShowStartPage = false; } } else { - this.OpenCommand.Execute(new Tuple(command.FilePath, command.ForceManualExecutionMode)); - this.ShowStartPage = false; + OpenCommand.Execute(new Tuple(command.FilePath, command.ForceManualExecutionMode)); + ShowStartPage = false; + } + } + + /// + /// Open a definition or workspace. + /// + /// + /// For most cases, parameters variable refers to the Json content file to open + /// However, when this command is used in OpenFileDialog, the variable is + /// a Tuple instead. The boolean flag is used to override the + /// RunSetting of the workspace. + private void OpenFromJson(object parameters) + { + // try catch for exceptions thrown while opening files, say from a future version, + // that can't be handled reliably + filePath = string.Empty; + fileContents = string.Empty; + bool forceManualMode = false; + try + { + var packedParams = parameters as Tuple; + if (packedParams != null) + { + fileContents = packedParams.Item1; + forceManualMode = packedParams.Item2; + } + else + { + fileContents = parameters as string; + } + ExecuteCommand(new DynamoModel.OpenFileFromJsonCommand(fileContents, forceManualMode)); + } + catch (Exception e) + { + if (!DynamoModel.IsTestMode) + { + string commandString = String.Format(Resources.MessageErrorOpeningFileGeneral); + string errorMsgString; + if (e is Newtonsoft.Json.JsonReaderException) + { + errorMsgString = String.Format(Resources.MessageFailedToOpenCorruptedFile, "Json file content"); + } + else + { + errorMsgString = String.Format(Resources.MessageUnkownErrorOpeningFile, "Json file content"); + } + model.Logger.LogNotification("Dynamo", commandString, errorMsgString, e.ToString()); + Wpf.Utilities.MessageBoxService.Show(errorMsgString, + Properties.Resources.FileLoadFailureMessageBoxTitle, + MessageBoxButton.OK, + MessageBoxImage.Exclamation); + } + else + { + throw (e); + } + return; } + this.ShowStartPage = false; // Hide start page if there's one. } /// @@ -1489,6 +1583,7 @@ private void Open(object parameters) // try catch for exceptions thrown while opening files, say from a future version, // that can't be handled reliably filePath = string.Empty; + fileContents = string.Empty; bool forceManualMode = false; try { @@ -1541,17 +1636,27 @@ private bool CanOpen(object parameters) return PathHelper.IsValidPath(filePath); } + private bool CanOpenFromJson(object parameters) + { + string fileContents = parameters as string; + return PathHelper.isFileContentsValidJson(fileContents, out _); + } + /// /// Read the contents of the file and set the view parameters for that current workspace /// private void model_ComputeModelDeserialized() - { - if (filePath == String.Empty) return; - string fileContents = File.ReadAllText(filePath); + { try { + string fileContentsInUse = String.IsNullOrEmpty(filePath) ? fileContents : File.ReadAllText(filePath); + if (string.IsNullOrEmpty(fileContentsInUse)) + { + return; + } + // This call will fail in the case of an XML file - ExtraWorkspaceViewInfo viewInfo = WorkspaceViewModel.ExtraWorkspaceViewInfoFromJson(fileContents); + ExtraWorkspaceViewInfo viewInfo = WorkspaceViewModel.ExtraWorkspaceViewInfoFromJson(fileContentsInUse); Model.CurrentWorkspace.UpdateWithExtraWorkspaceViewInfo(viewInfo); Model.OnWorkspaceOpening(viewInfo); diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs index 5594266e77a..66cb21b209b 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs @@ -12,6 +12,8 @@ private void InitializeDelegateCommands() { OpenCommand = new DelegateCommand(Open, CanOpen); OpenIfSavedCommand = new DelegateCommand(OpenIfSaved, CanOpen); + OpenFromJsonCommand = new DelegateCommand(OpenFromJson, CanOpenFromJson); + OpenFromJsonIfSavedCommand = new DelegateCommand(OpenFromJsonIfSaved, CanOpenFromJson); OpenRecentCommand = new DelegateCommand(OpenRecent, CanOpenRecent); SaveCommand = new DelegateCommand(Save, CanSave); SaveAsCommand = new DelegateCommand(SaveAs, CanSaveAs); @@ -84,6 +86,8 @@ private void InitializeDelegateCommands() NodeFromSelectionCommand = new DelegateCommand(CreateNodeFromSelection, CanCreateNodeFromSelection); OpenDocumentationLinkCommand = new DelegateCommand(OpenDocumentationLink); } + public DelegateCommand OpenFromJsonIfSavedCommand { get; set; } + public DelegateCommand OpenFromJsonCommand { get; set; } public DelegateCommand OpenIfSavedCommand { get; set; } public DelegateCommand OpenCommand { get; set; } public DelegateCommand ShowOpenDialogAndOpenResultCommand { get; set; } diff --git a/src/DynamoUtilities/DebugModes.cs b/src/DynamoUtilities/DebugModes.cs index 666e933edcc..47cc1982582 100644 --- a/src/DynamoUtilities/DebugModes.cs +++ b/src/DynamoUtilities/DebugModes.cs @@ -52,6 +52,8 @@ private static void RegisterDebugModes() internal static void LoadDebugModesStatusFromConfig(string configPath) { + if (!File.Exists(configPath)) return; + try { XmlDocument xmlDoc; diff --git a/src/DynamoUtilities/PathHelper.cs b/src/DynamoUtilities/PathHelper.cs index 201f3491403..fdd4f6e833c 100644 --- a/src/DynamoUtilities/PathHelper.cs +++ b/src/DynamoUtilities/PathHelper.cs @@ -128,27 +128,54 @@ public static bool isValidXML(string path, out XmlDocument xmlDoc, out Exception } /// - /// This is a utility method for checking if given path contains valid Json document. + /// This is a utility method for checking if a given string represents a valid Json document. /// - /// path to the target json file /// string contents of target json file - /// Return true if file is Json, false if file is not Json, exception as out param - public static bool isValidJson(string path, out string fileContents, out Exception ex) + /// Return true if fileContents is Json, false if file is not Json, exception as out param + public static bool isFileContentsValidJson(string fileContents, out Exception ex) { - fileContents = ""; + ex = null; + if (string.IsNullOrEmpty(fileContents)) + { + ex = new JsonReaderException(); + return false; + } + try { - fileContents = File.ReadAllText(path); fileContents = fileContents.Trim(); if ((fileContents.StartsWith("{") && fileContents.EndsWith("}")) || //For object (fileContents.StartsWith("[") && fileContents.EndsWith("]"))) //For array { var obj = Newtonsoft.Json.Linq.JToken.Parse(fileContents); - ex = null; return true; } - ex = new JsonReaderException(); - return false; + else + { + ex = new JsonReaderException(); + } + } + catch(Exception e) + { + ex = e; + } + + return false; + } + + /// + /// This is a utility method for checking if given path contains valid Json document. + /// + /// path to the target json file + /// string contents of target json file + /// Return true if file is Json, false if file is not Json, exception as out param + public static bool isValidJson(string path, out string fileContents, out Exception ex) + { + fileContents = ""; + try + { + fileContents = File.ReadAllText(path); + return isFileContentsValidJson(fileContents, out ex); } catch (Exception e) //some other exception { diff --git a/test/DynamoCoreTests/Models/OpenFileFromJsonCommandTest.cs b/test/DynamoCoreTests/Models/OpenFileFromJsonCommandTest.cs new file mode 100644 index 00000000000..379eb595504 --- /dev/null +++ b/test/DynamoCoreTests/Models/OpenFileFromJsonCommandTest.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Xml; +using Dynamo.Models; +using Dynamo.Utilities; +using NUnit.Framework; +using static Dynamo.Models.DynamoModel; + +namespace Dynamo.Tests.ModelsTest +{ + [TestFixture] + class OpenFileFromJsonCommandTest : DynamoModelTestBase + { + /// + /// This test method will execute the command OpenFileFromJsonCommand + /// + [Test] + [Category("UnitTests")] + public void RunOpenFileFromJsonCommandTest() + { + //Arrange + string samplepath = Path.Combine(TestDirectory, @"core\callsite\RebindingSingleDimension.dyn"); + string fileContents = File.ReadAllText(samplepath); + var openFromJsonCommand = new OpenFileFromJsonCommand(fileContents, true); + + //Act + CurrentDynamoModel.ExecuteCommand(openFromJsonCommand); + + XmlDocument xmlDocument = new XmlDocument(); + XmlElement elemTest = xmlDocument.CreateElement("XmlFileContents"); + var helper = new XmlElementHelper(elemTest); + + helper.SetAttribute("XmlFileContents", fileContents); + + var deserializedCommand = OpenFileFromJsonCommand.DeserializeCore(elemTest); + + + //Assert + Assert.IsEmpty(CurrentDynamoModel.CurrentWorkspace.FileName); + Assert.IsTrue(CurrentDynamoModel.CurrentWorkspace.HasUnsavedChanges); + Assert.IsNotNull(deserializedCommand); + } + } +}