//---------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------- namespace System.Activities.Presentation { using System; using System.Activities.Debugger; using System.Activities.Presentation.Hosting; using System.Activities.Presentation.Model; using System.Activities.Presentation.View; using System.Activities.Presentation.Xaml; using System.Activities.Statements; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Runtime; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.ServiceModel.Activities; using System.Windows; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Xaml; using Microsoft.Activities.Presentation; using Microsoft.Activities.Presentation.Xaml; public static class CutCopyPasteHelper { internal static readonly DependencyProperty ChildContainersProperty = DependencyProperty.RegisterAttached("ChildContainers", typeof(HashSet), typeof(CutCopyPasteHelper), new UIPropertyMetadata(null)); static object workflowCallbackContext = null; internal const string WorkflowClipboardFormat = "WorkflowXamlFormat"; internal const string WorkflowClipboardFormat_TargetFramework = "WorkflowXamlFormat_TargetFramework"; //define a workflow callback clipboard format - make it unique across all processes static readonly string WorkflowCallbackClipboardFormat = string.Format(CultureInfo.InvariantCulture, "WorkflowCallbackFormat{0}", Guid.NewGuid()); const string versionInfo = "1.0"; static IList disallowedTypesForCopy; static IEnumerable DisallowedTypesForCopy { get { if (null == disallowedTypesForCopy) { disallowedTypesForCopy = new List(); disallowedTypesForCopy.Add(typeof(ActivityBuilder)); disallowedTypesForCopy.Add(typeof(ModelItemKeyValuePair<,>)); disallowedTypesForCopy.Add(typeof(WorkflowService)); disallowedTypesForCopy.Add(typeof(Catch)); } return disallowedTypesForCopy; } } internal static void AddDisallowedTypeForCopy(Type type) { if (!DisallowedTypesForCopy.Any(p => type == p)) { disallowedTypesForCopy.Add(type); } } [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] static void AddChildContainer(WorkflowViewElement viewElement, ICompositeView sourceContainer) { if (viewElement == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("viewElement")); } if (sourceContainer == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("sourceContainer")); } HashSet containers = (HashSet)viewElement.GetValue(CutCopyPasteHelper.ChildContainersProperty); if (containers == null) { containers = new HashSet(); viewElement.SetValue(CutCopyPasteHelper.ChildContainersProperty, containers); } containers.Add(sourceContainer); } [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] static HashSet GetChildContainers(WorkflowViewElement workflowViewElement) { HashSet childContainers = null; if (workflowViewElement != null && workflowViewElement.ShowExpanded) { childContainers = (HashSet)workflowViewElement.GetValue(CutCopyPasteHelper.ChildContainersProperty); } return childContainers; } //This enables us to get children ICompositeViews from WorkflowViewElements. //Eg. get the WorkflowItemsPresenter from SequenceDesigner. //This is useful for Cut-Copy-Paste, Delete handling, etc. internal static void RegisterWithParentViewElement(ICompositeView container) { WorkflowViewElement parent = GetParentViewElement(container); if (parent != null) { CutCopyPasteHelper.AddChildContainer(parent, container); } } //Returns the first WorkflowViewElement in the parent chain. //If ICompositeView is a WorkflowViewElement this method returns the same object. static WorkflowViewElement GetParentViewElement(ICompositeView container) { DependencyObject parent = container as DependencyObject; return GetParentViewElement(parent); } //Returns the first WorkflowViewElement in the parent chain. //Move this to a helper class. internal static WorkflowViewElement GetParentViewElement(DependencyObject obj) { while (obj != null && !(obj is WorkflowViewElement)) { obj = VisualTreeHelper.GetParent(obj); } return obj as WorkflowViewElement; } internal static IList SortFromMetaData(IList itemsToPaste, List metaData) { IList mergedItemsToPaste = SortFromMetaDataOnly(metaData); // append items that are not sorted foreach (object itemToPaste in itemsToPaste) { if (!mergedItemsToPaste.Contains(itemToPaste)) { mergedItemsToPaste.Add(itemToPaste); } } return mergedItemsToPaste; } internal static IList SortFromMetaDataOnly(List metaData) { List mergedItemsToPaste = new List(); if (metaData == null) { return mergedItemsToPaste; } foreach (object metaDataObject in metaData) { List orderedItemsMetaData = metaDataObject as List; if (orderedItemsMetaData == null) { continue; } foreach (object objectToPaste in orderedItemsMetaData) { if (!mergedItemsToPaste.Contains(objectToPaste)) { mergedItemsToPaste.Add(objectToPaste); } } } return mergedItemsToPaste; } public static void DoCut(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } Selection currentSelection = context.Items.GetValue(); List modelItemsToCut = new List(currentSelection.SelectedObjects); CutCopyPasteHelper.DoCut(modelItemsToCut, context); } internal static void DoCut(List modelItemsToCut, EditingContext context) { if (modelItemsToCut == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("modelItemsToCut")); } if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } modelItemsToCut.RemoveAll((modelItem) => { return modelItem == null; }); if (modelItemsToCut.Count > 0) { using (EditingScope es = (EditingScope)modelItemsToCut[0].BeginEdit(SR.CutOperationEditingScopeDescription)) { try { CutCopyOperation(modelItemsToCut, context, true); } catch (ExternalException e) { es.Revert(); ErrorReporting.ShowErrorMessage(e.Message); return; } DesignerView view = context.Services.GetService(); //Setting the selection to Breadcrumb root. Fx.Assert(view != null, "DesignerView Cannot be null during cut"); WorkflowViewElement rootView = view.RootDesigner as WorkflowViewElement; if (rootView != null) { Selection.SelectOnly(context, rootView.ModelItem); } es.Complete(); } } } public static void DoCopy(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } Selection currentSelection = context.Items.GetValue(); List modelItemsToCopy = new List(currentSelection.SelectedObjects); CutCopyPasteHelper.DoCopy(modelItemsToCopy, context); } private static void DoCopy(List modelItemsToCopy, EditingContext context) { if (modelItemsToCopy == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("modelItemsToCopy")); } if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } // copy only works if we have DesignerView up and running so check and throw here if (context.Services.GetService() == null) { throw FxTrace.Exception.AsError(new InvalidOperationException(SR.CutCopyRequiresDesignerView)); } modelItemsToCopy.RemoveAll((modelItem) => { return modelItem == null; }); try { CutCopyOperation(modelItemsToCopy, context, false); } catch (ExternalException e) { ErrorReporting.ShowErrorMessage(e.Message); } } static void CutCopyOperation(List modelItemsToCutCopy, EditingContext context, bool isCutOperation) { List objectsOnClipboard = null; List metaData = null; if (modelItemsToCutCopy.Count > 0) { objectsOnClipboard = new List(modelItemsToCutCopy.Count); metaData = new List(); Dictionary> notifyDictionary = new Dictionary>(); UIElement breadCrumbRootView = ((DesignerView)context.Services.GetService()).RootDesigner; foreach (ModelItem modelItem in modelItemsToCutCopy) { object currentElement = modelItem.GetCurrentValue(); if (typeof(Activity).IsAssignableFrom(currentElement.GetType())) { string fileName; if (AttachablePropertyServices.TryGetProperty(currentElement, XamlDebuggerXmlReader.FileNameName, out fileName)) { AttachablePropertyServices.RemoveProperty(currentElement, XamlDebuggerXmlReader.FileNameName); } } if (modelItem.View != null) { //The case where the breadcrumbroot designer is Cut/Copied. We do not delete the root designer, we only copy it. if (breadCrumbRootView.Equals(modelItem.View)) { notifyDictionary.Clear(); objectsOnClipboard.Add(modelItem.GetCurrentValue()); break; } else { ICompositeView container = (ICompositeView)DragDropHelper.GetCompositeView((WorkflowViewElement)modelItem.View); if (container != null) { //If the parent and some of its children are selected and cut/copied, we ignore the children. //The entire parent will be cut/copied. //HashSet parentModelItems contains all the model items in the parent chain of current modelItem. //We use HashSet.IntersectWith operation to determine if one of the parents is set to be cut. HashSet parentModelItems = CutCopyPasteHelper.GetSelectableParentModelItems(modelItem); parentModelItems.IntersectWith(modelItemsToCutCopy); if (parentModelItems.Count == 0) { if (!notifyDictionary.ContainsKey(container)) { notifyDictionary[container] = new List(); } notifyDictionary[container].Add(modelItem); } } } } } foreach (ICompositeView container in notifyDictionary.Keys) { object containerMetaData = false; if (isCutOperation) { containerMetaData = container.OnItemsCut(notifyDictionary[container]); } else { containerMetaData = container.OnItemsCopied(notifyDictionary[container]); } if (containerMetaData != null) { metaData.Add(containerMetaData); } //Put the actual activities and not the modelItems in the clipboard. foreach (ModelItem modelItem in notifyDictionary[container]) { objectsOnClipboard.Add(modelItem.GetCurrentValue()); } } if (metaData.Count == 0) { metaData = null; } } try { FrameworkName targetFramework = context.Services.GetService().TargetFrameworkName; PutOnClipBoard(objectsOnClipboard, metaData, targetFramework); } catch (XamlObjectReaderException exception) { if (modelItemsToCutCopy.Count > 0 && ErrorActivity.GetHasErrorActivities(modelItemsToCutCopy[0].Root.GetCurrentValue())) { ErrorReporting.ShowErrorMessage(SR.CutCopyErrorActivityMessage); } else { ErrorReporting.ShowErrorMessage(exception.Message); } } } //This method collects all the ModelItems in the parent chain by calling the GetSelectableParentViewElements method //which walks the WPF Visual tree. We want to avoid walking ModelItem tree. internal static HashSet GetSelectableParentModelItems(ModelItem modelItem) { if (null == modelItem) { throw FxTrace.Exception.ArgumentNull("modelItem"); } List parentViewElements = GetSelectableParentViewElements(modelItem.View as WorkflowViewElement); HashSet parentModelItems = new HashSet(); foreach (WorkflowViewElement view in parentViewElements) { parentModelItems.Add(view.ModelItem); } return parentModelItems; } //This is more efficient than walking up the VisualTree looking for WorkflowViewElements. //Assuming that Cut-Copy will always be against selected elements. //This implies that only elements under the BreadCrumbRoot can be cut/copied. static List GetSelectableParentViewElements(WorkflowViewElement childElement) { List parentViewElements = new List(); if (childElement != null) { UIElement breadcrumbRoot = childElement.Context.Services.GetService().RootDesigner; ICompositeView container = (ICompositeView)DragDropHelper.GetCompositeView(childElement); while (!childElement.Equals(breadcrumbRoot) && container != null) { childElement = CutCopyPasteHelper.GetParentViewElement(container); Fx.Assert(childElement != null, "container should be present in a WorkflowViewElement"); parentViewElements.Add(childElement); container = (ICompositeView)DragDropHelper.GetCompositeView(childElement); } } return parentViewElements; } public static void DoPaste(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } DoPaste(context, new Point(-1, -1), null); } internal static void DoPaste(EditingContext context, Point pastePoint, WorkflowViewElement pastePointReference) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } ModelItem modelItem = context.Items.GetValue().PrimarySelection; if (modelItem == null) { return; } //Get data from clipboard. List metaData = null; List clipboardObjects = GetFromClipboard(out metaData, context); if (clipboardObjects != null) { using (EditingScope es = (EditingScope)modelItem.BeginEdit(SR.PasteUndoDescription)) { if (clipboardObjects.Count == 3 && clipboardObjects[1] is Func) { var factoryMethod = (Func)clipboardObjects[1]; object result = factoryMethod(modelItem, clipboardObjects[2]); clipboardObjects = new List(); clipboardObjects.Add(result); } ICompositeView container = GetContainerForPaste(modelItem, pastePoint); if (container != null) { container.OnItemsPasted(clipboardObjects, metaData, pastePoint, pastePointReference); } es.Complete(); } } } static ICompositeView GetClickedContainer(ModelItem clickedModelItem, Point clickPoint) { Visual parentVisual = clickedModelItem.View as Visual; if (parentVisual == null) { return null; } DependencyObject visualHit = null; HitTestResult hitTest = VisualTreeHelper.HitTest(parentVisual, clickPoint); if (hitTest != null) { visualHit = hitTest.VisualHit; while (visualHit != null && !visualHit.Equals(parentVisual) && !typeof(ICompositeView).IsAssignableFrom(visualHit.GetType())) { visualHit = VisualTreeHelper.GetParent(visualHit); } } return visualHit as ICompositeView; } static ICompositeView GetContainerForPaste(ModelItem pasteModelItem, Point clickPoint) { ICompositeView pasteContainer = null; if (null != pasteModelItem && null != pasteModelItem.View && pasteModelItem.View is WorkflowViewElement) { pasteContainer = ((WorkflowViewElement)pasteModelItem.View).ActiveCompositeView; } if (null == pasteContainer) { //Get clicked container. if (clickPoint.X > 0 && clickPoint.Y > 0) { pasteContainer = GetClickedContainer(pasteModelItem, clickPoint); } //If the container itself is a WVE, there's posibility that it's collapsed. //Thus, we need to check this as well. if (pasteContainer != null && pasteContainer is WorkflowViewElement) { WorkflowViewElement view = pasteContainer as WorkflowViewElement; if (!view.ShowExpanded) { pasteContainer = null; } } else if (pasteContainer == null) //If the modelitem.View itself is a container. { WorkflowViewElement view = pasteModelItem.View as WorkflowViewElement; if (view != null && view.ShowExpanded) { pasteContainer = pasteModelItem.View as ICompositeView; } } //Get the container registered with modelItem.View if unambigous //If nothing works take the container with keyboard focus if one exists. if (pasteContainer == null) { HashSet childrenContainers = CutCopyPasteHelper.GetChildContainers(pasteModelItem.View as WorkflowViewElement); if ((childrenContainers == null || childrenContainers.Count == 0) && null != pasteModelItem.View) { pasteContainer = (ICompositeView)DragDropHelper.GetCompositeView((WorkflowViewElement)pasteModelItem.View); } else if (null != childrenContainers && childrenContainers.Count == 1) { pasteContainer = new List(childrenContainers)[0]; } else { pasteContainer = Keyboard.FocusedElement as ICompositeView; } } } return pasteContainer; } private static void PutOnClipBoard(List selectedData, List metaData, FrameworkName targetFramework) { CutCopyPasteHelper.workflowCallbackContext = null; if (selectedData != null) { ClipboardData clipboardData = new ClipboardData(); clipboardData.Data = selectedData; clipboardData.Metadata = metaData; clipboardData.Version = versionInfo; XamlReader reader = ViewStateXamlHelper.RemoveIdRefs(new XamlObjectReader(clipboardData)); StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture); XamlServices.Transform(reader, new XamlXmlWriter(stringWriter, reader.SchemaContext), true); string clipBoardString = stringWriter.ToString(); DataObject dataObject = new DataObject(WorkflowClipboardFormat, clipBoardString); dataObject.SetData(DataFormats.Text, clipBoardString); dataObject.SetData(WorkflowClipboardFormat_TargetFramework, targetFramework); RetriableClipboard.SetDataObject(dataObject, true); } } //PutCallbackOnClipBoard - tries to put into private (this application only) clipboard a callback //to a method. The method will be invoked when user retrieves clipboard content - i.e. by //calling a paste command. //the callback has to be: //- static method //- have return value (not void) //- takes 2 input parameters: // * 1 parameter is modelitem - this is a target modelitem upon which callback is to be executed // * 2 parameter is user provided context - any object. Since this callback will be executed within // this application only, there is no need for context to be serializable. internal static void PutCallbackOnClipBoard(Func callbackMethod, Type callbackResultType, object context) { if (null == callbackMethod || null == context) { throw FxTrace.Exception.AsError(new ArgumentNullException(null == callbackMethod ? "callbackMethod" : "context")); } ClipboardData clipboardData = new ClipboardData(); List data = new List(); data.Add(callbackResultType); data.Add(callbackMethod); clipboardData.Data = data; clipboardData.Version = versionInfo; CutCopyPasteHelper.workflowCallbackContext = context; try { RetriableClipboard.SetDataObject(new DataObject(WorkflowCallbackClipboardFormat, clipboardData, false), false); } catch (ExternalException e) { ErrorReporting.ShowErrorMessage(e.Message); } } private static FrameworkName GetTargetFrameworkFromClipboard(DataObject dataObject) { Fx.Assert(dataObject != null, "dataObject should not be null"); FrameworkName clipboardFrameworkName = null; if (dataObject.GetDataPresent(WorkflowClipboardFormat_TargetFramework)) { clipboardFrameworkName = TryGetData(dataObject, WorkflowClipboardFormat_TargetFramework) as FrameworkName; } if (clipboardFrameworkName == null) { clipboardFrameworkName = FrameworkNameConstants.NetFramework40; } return clipboardFrameworkName; } //This method returns the list of objects put on clipboard by cut/copy. //Out parameter is the metaData information. [SuppressMessage(FxCop.Category.Design, FxCop.Rule.DoNotCatchGeneralExceptionTypes, Justification = "Deserialization of cliboard data might fail. Propagating exceptions might lead to VS crash.")] [SuppressMessage("Reliability", "Reliability108", Justification = "Deserialization of cliboard data might fail. Propagating exceptions might lead to VS crash.")] private static List GetFromClipboard(out List metaData, EditingContext editingContext) { Fx.Assert(editingContext != null, "editingContext should not be null"); MultiTargetingSupportService multiTargetingService = editingContext.Services.GetService() as MultiTargetingSupportService; DesignerConfigurationService config = editingContext.Services.GetService(); DataObject dataObject = RetriableClipboard.GetDataObject() as DataObject; List workflowData = null; metaData = null; if (dataObject != null) { if (dataObject.GetDataPresent(WorkflowClipboardFormat)) { bool isCopyingFromHigherFrameworkToLowerFramework = false; if (multiTargetingService != null && config != null) { isCopyingFromHigherFrameworkToLowerFramework = GetTargetFrameworkFromClipboard(dataObject).Version > config.TargetFrameworkName.Version; } string clipBoardString = (string)TryGetData(dataObject, WorkflowClipboardFormat); using (StringReader stringReader = new StringReader(clipBoardString)) { try { XamlSchemaContext schemaContext; if (isCopyingFromHigherFrameworkToLowerFramework) { schemaContext = new MultiTargetingXamlSchemaContext(multiTargetingService); } else { schemaContext = new XamlSchemaContext(); } using (XamlXmlReader xamlXmlReader = new XamlXmlReader(stringReader, schemaContext)) { ClipboardData clipboardData = (ClipboardData)XamlServices.Load(xamlXmlReader); metaData = clipboardData.Metadata; workflowData = clipboardData.Data; } } catch (Exception e) { Trace.WriteLine(e.Message); } } } else if (dataObject.GetDataPresent(WorkflowCallbackClipboardFormat)) { ClipboardData localData = (ClipboardData)TryGetData(dataObject, WorkflowCallbackClipboardFormat); metaData = null; workflowData = localData.Data; workflowData.Add(CutCopyPasteHelper.workflowCallbackContext); } } return workflowData; } private static object TryGetData(DataObject dataObject, string dataFormat) { try { return dataObject.GetData(dataFormat); } catch (OutOfMemoryException) { Trace.TraceError("OutOfMemoryException thrown from DataObject."); } return null; } private static bool CanCopy(Type type) { foreach (Type disallowedType in CutCopyPasteHelper.DisallowedTypesForCopy) { if (disallowedType.IsAssignableFrom(type)) { return false; } if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(disallowedType)) { return false; } } return true; } private static bool CanCopy(ModelItem item) { return null != item.View && item.View is WorkflowViewElement && null != ((WorkflowViewElement)item.View).ModelItem && CanCopy(((WorkflowViewElement)item.View).ModelItem.ItemType); } public static bool CanCopy(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } Selection selection = context.Items.GetValue(); return selection.SelectionCount > 0 && selection.SelectedObjects.All(p => CanCopy(p)); } public static bool CanCut(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } bool result = false; Selection selection = context.Items.GetValue(); if (null != selection && selection.SelectionCount > 0) { DesignerView designerView = context.Services.GetService(); result = selection.SelectedObjects.All(p => CanCopy(p) && !p.View.Equals(designerView.RootDesigner)); } return result; } public static bool CanPaste(EditingContext context) { if (context == null) { throw FxTrace.Exception.AsError(new ArgumentNullException("context")); } bool result = false; ModelItem primarySelection = context.Items.GetValue().PrimarySelection; if (null != primarySelection) { ICompositeView container = GetContainerForPaste(primarySelection, new Point(-1, -1)); if (null != container) { DataObject dataObject = RetriableClipboard.GetDataObject() as DataObject; if (null != dataObject) { List metaData = null; List itemsToPaste = null; try { if (dataObject.GetDataPresent(WorkflowClipboardFormat)) { itemsToPaste = GetFromClipboard(out metaData, context); result = container.CanPasteItems(itemsToPaste); } else if (dataObject.GetDataPresent(WorkflowCallbackClipboardFormat)) { itemsToPaste = GetFromClipboard(out metaData, context); result = container.CanPasteItems(itemsToPaste.GetRange(0, 1)); } } //This is being defensive for the case where user code for CanPasteITems throws a non-fatal exception. catch (Exception exp) { if (Fx.IsFatal(exp)) { throw; } } } } } return result; } } }