File: System\Activities\Core\Presentation\TransitionDesigner.xaml.cs
Project: ndp\cdf\src\NetFx40\Tools\System.Activities.Core.Presentation\System.Activities.Core.Presentation.csproj (System.Activities.Core.Presentation)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
namespace System.Activities.Core.Presentation
{
    using System.Activities.Presentation;
    using System.Activities.Presentation.FreeFormEditing;
    using System.Activities.Presentation.Metadata;
    using System.Activities.Presentation.Model;
    using System.Activities.Presentation.View;
    using System.Activities.Presentation.View.OutlineView;
    using System.Activities.Statements;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime;
    using System.Windows;
    using System.Windows.Controls.Primitives;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Activities.Presentation.Internal.PropertyEditing;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
 
    partial class TransitionDesigner
    {
        public static readonly DependencyProperty NewTransitionProperty = DependencyProperty.Register("NewTransition", typeof(object), typeof(TransitionDesigner), new PropertyMetadata(null));
        public static readonly DependencyProperty TransitionsSharingTriggerProperty = DependencyProperty.Register("TransitionsSharingTrigger", typeof(ObservableCollection<ExpandableItemWrapper>), typeof(TransitionDesigner), new PropertyMetadata(null));
        public static readonly DependencyProperty SelectedTransitionProperty = DependencyProperty.Register("SelectedTransition", typeof(ExpandableItemWrapper), typeof(TransitionDesigner), new PropertyMetadata(null));
 
        internal const string TriggerPropertyName = "Trigger";
        internal const string ActionPropertyName = "Action";
        internal const string ToPropertyName = "To";
        internal const string ConditionPropertyName = "Condition";
        const string ExpandViewStateKey = "IsExpanded";
        const int TotalFreeConnectionPointNum = StateContainerEditor.ConnectionPointNum * 4;
 
        private ModelItem parentStateModelItem = null;
        private bool suppressUpdatingTransitionsSharingTrigger = false;
        private bool isPopulated = false;
        private CaseKeyBox addNewTransitionBox = null;
        private TextBlock addNewTransitionLabel = null;
 
        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public TransitionDesigner()
        {
            InitializeComponent();
            this.TransitionsSharingTrigger = new ObservableCollection<ExpandableItemWrapper>();
 
            this.Loaded += (sender, e) =>
            {
                if (!this.isPopulated)
                {
                    this.isPopulated = true;
                    this.TransitionsSharingTrigger.CollectionChanged += OnTransitionsCollectionChanged;
                    this.ModelItem.PropertyChanged += OnModelItemPropertyChanged;
                    this.parentStateModelItem = StateContainerEditor.GetParentStateModelItemForTransition(this.ModelItem);
                    this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection.CollectionChanged += OnTransitionsModelItemCollectionChanged;
                    ExpandableItemWrapper selectedItem = this.UpdateTransitionsSharingTrigger();
                    if (null != selectedItem)
                    {
                        this.SelectedTransition = selectedItem;
                    }
                }
            };
 
            this.Unloaded += (sender, e) =>
            {
                if (this.isPopulated)
                {
                    this.isPopulated = false;
                    this.TransitionsSharingTrigger.Clear();
                    this.TransitionsSharingTrigger.CollectionChanged -= OnTransitionsCollectionChanged;
                    this.ModelItem.PropertyChanged -= OnModelItemPropertyChanged;
                    this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection.CollectionChanged -= OnTransitionsModelItemCollectionChanged;
                    this.SelectedTransition = null;
                    this.parentStateModelItem = null;
                }
            };
        }
 
        public object NewTransition
        {
            get { return (object)this.GetValue(NewTransitionProperty); }
            set { this.SetValue(NewTransitionProperty, value); }
        }
 
        public ObservableCollection<ExpandableItemWrapper> TransitionsSharingTrigger
        {
            get { return (ObservableCollection<ExpandableItemWrapper>)this.GetValue(TransitionsSharingTriggerProperty); }
            set { this.SetValue(TransitionsSharingTriggerProperty, value); }
        }
 
        public ExpandableItemWrapper SelectedTransition 
        {
            get { return (ExpandableItemWrapper)this.GetValue(SelectedTransitionProperty); }
            set { this.SetValue(SelectedTransitionProperty, value); }
        }
 
        private void OnNewTransitionLoaded(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionBox = (CaseKeyBox)sender;
        }
 
        private void OnNewTransitionUnloaded(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionBox = null;
        }
 
        private ExpandableItemWrapper UpdateTransitionsSharingTrigger()
        {
            ExpandableItemWrapper wrapper = null;
            if (!this.suppressUpdatingTransitionsSharingTrigger)
            {
                this.TransitionsSharingTrigger.Clear();
                bool expandTargetTransition = true;
                object expandCollapseTargetViewState = this.ViewStateService.RetrieveViewState(this.ModelItem, ExpandViewStateKey);
 
                if (expandCollapseTargetViewState != null)
                {
                    expandTargetTransition = (bool)expandCollapseTargetViewState;
                }
 
                wrapper = new ExpandableItemWrapper()
                {
                    Item = this.ModelItem,
                    IsExpanded = expandTargetTransition
                };
                ModelItem triggerModelItem = this.ModelItem.Properties[TriggerPropertyName].Value;
                if (triggerModelItem != null)
                {
                    foreach (ModelItem transitionModelItem in this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection)
                    {
                        if (transitionModelItem != this.ModelItem)
                        {
                            if (triggerModelItem == transitionModelItem.Properties[TriggerPropertyName].Value)
                            {
                                bool expandTransition = false;
                                object expandCollapseViewState = this.ViewStateService.RetrieveViewState(transitionModelItem, ExpandViewStateKey);
                                if (expandCollapseViewState != null)
                                {
                                    expandTransition = (bool)expandCollapseViewState;
                                }
 
                                this.TransitionsSharingTrigger.Add(new ExpandableItemWrapper()
                                {
                                    Item = transitionModelItem,
                                    IsExpanded = expandTransition
                                });
                            }
                        }
                        else
                        {
                            this.TransitionsSharingTrigger.Add(wrapper);
                        }
                    }
                }
                // Connectors starting from the same point should share the same trigger
                else
                {
                    PointCollection thisPointCollection = this.ViewStateService.RetrieveViewState(this.ModelItem, StateContainerEditor.ConnectorLocationViewStateKey) as PointCollection;
                    if (thisPointCollection != null && thisPointCollection.Count > 1)
                    {
                        foreach (ModelItem transitionModelItem in this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection)
                        {
                            if (transitionModelItem != this.ModelItem)
                            {
                                PointCollection pointCollection = this.ViewStateService.RetrieveViewState(transitionModelItem, StateContainerEditor.ConnectorLocationViewStateKey) as PointCollection;
                                if (pointCollection != null && pointCollection.Count > 0)
                                {
                                    if (pointCollection[0].IsEqualTo(thisPointCollection[0]))
                                    {
                                        Fx.Assert(transitionModelItem.Properties[TriggerPropertyName].Value == null, "Transition trigger should be null.");
                                        bool expandTransition = false;
                                        object expandCollapseViewState = this.ViewStateService.RetrieveViewState(transitionModelItem, ExpandViewStateKey);
 
                                        if (expandCollapseViewState != null)
                                        {
                                            expandTransition = (bool)expandCollapseViewState;
                                        }
 
                                        this.TransitionsSharingTrigger.Add(new ExpandableItemWrapper()
                                        {
                                            Item = transitionModelItem,
                                            IsExpanded = expandTransition
                                        });
                                    }
                                }
                            }
                            else
                            {
                                this.TransitionsSharingTrigger.Add(wrapper);
                            }
                        }
                    }
                }
            }
 
            return wrapper;
        }
 
        public static void RegisterMetadata(AttributeTableBuilder builder)
        {
            Type transitionType = typeof(Transition);
            builder.AddCustomAttributes(transitionType, new DesignerAttribute(typeof(TransitionDesigner)));
            builder.AddCustomAttributes(transitionType, transitionType.GetProperty(TransitionDesigner.TriggerPropertyName), BrowsableAttribute.No);
            builder.AddCustomAttributes(transitionType, transitionType.GetProperty(TransitionDesigner.ActionPropertyName), BrowsableAttribute.No);
            builder.AddCustomAttributes(transitionType, transitionType.GetProperty(TransitionDesigner.ToPropertyName), BrowsableAttribute.No);
            builder.AddCustomAttributes(transitionType, transitionType.GetProperty(TransitionDesigner.ConditionPropertyName), new HidePropertyInOutlineViewAttribute());
        }
 
        private static void SwapItems(ModelItemCollection collection, ModelItem modelItem1, ModelItem modelItem2)
        {
            int index1 = collection.IndexOf(modelItem1);
            int index2 = collection.IndexOf(modelItem2);
            collection.Remove(modelItem1);
            collection.Insert(index2, modelItem1);
            collection.Remove(modelItem2);
            collection.Insert(index1, modelItem2);
        }
 
        private void OnTransitionsModelItemCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.UpdateTransitionsSharingTrigger();
        }
 
        private void OnModelItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // Update the Trigger property for all transitions that share the trigger
            if (e.PropertyName.Equals(TriggerPropertyName) && this.TransitionsSharingTrigger.Count > 0)
            {
                foreach (ExpandableItemWrapper wrapper in this.TransitionsSharingTrigger)
                {
                    if (wrapper.Item != this.ModelItem)
                    {
                        wrapper.Item.Properties[TriggerPropertyName].SetValue(this.ModelItem.Properties[TriggerPropertyName].Value);
                    }
                }
            }
        }
 
        private void OnTransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Move)
            {
                try
                {
                    // We are updating the Transitions collection in the parent State in response to
                    // the changes in this.TransitionsSharingTrigger. We don't want to update it again
                    // to introduce a dead loop
                    this.suppressUpdatingTransitionsSharingTrigger = true;
                    using (EditingScope scope = (EditingScope)this.ModelItem.BeginEdit(SR.ReorderItems))
                    {
                        ModelItem movedModelItem = this.TransitionsSharingTrigger[e.NewStartingIndex].Item;
                        ModelItemCollection transitionsCollection = this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection;
                        // moving down
                        if (e.OldStartingIndex < e.NewStartingIndex)
                        {
                            ModelItem nextModelItem = this.TransitionsSharingTrigger[e.OldStartingIndex].Item;
                            SwapItems(transitionsCollection, movedModelItem, nextModelItem);
                        }
                        // moving up
                        else if (e.OldStartingIndex > e.NewStartingIndex)
                        {
                            ModelItem previousModelItem = this.TransitionsSharingTrigger[e.OldStartingIndex].Item;
                            SwapItems(transitionsCollection, previousModelItem, movedModelItem);
                        }
                        this.Context.Services.GetService<ModelTreeManager>().AddToCurrentEditingScope(new TransitionReorderChange());
                        scope.Complete();
                    }
                }
                finally
                {
                    this.suppressUpdatingTransitionsSharingTrigger = false;
                }
            }
        }
 
        private void OnDestinationStateClicked(object sender, RoutedEventArgs e)
        {
            Button button = (Button)sender;
            ModelItem destinationState = button.Tag as ModelItem;
            if (destinationState != null)
            {
                this.Designer.MakeRootDesigner(destinationState);
            }
        }
 
        private void OnSourceStateClicked(object sender, RoutedEventArgs e)
        {
            ModelItem sourceState = StateContainerEditor.GetParentStateModelItemForTransition(this.ModelItem);
            if (sourceState != null)
            {
                this.Designer.MakeRootDesigner(sourceState);
            }
        }
 
        private void OnExpandCollapseButtonClicked(object sender, RoutedEventArgs e)
        {
            ToggleButton button = (ToggleButton)sender;
            ListBox listBox = VisualTreeUtils.FindVisualAncestor<ListBox>(button);
            ExpandableItemWrapper wrapper = (ExpandableItemWrapper)listBox.SelectedItem;
            wrapper.IsExpanded = button.IsChecked.Value;
            this.ViewStateService.StoreViewState(wrapper.Item, ExpandViewStateKey, button.IsChecked.Value);
 
            if ((wrapper.IsExpanded && this.Designer.ShouldCollapseAll) || !wrapper.IsExpanded && this.Designer.ShouldExpandAll)
            {
                // Pin the item so that it can still be expanded / collapsed when CollapseAll / ExpandAll is enabled
                wrapper.IsPinned = true;
            }
        }
 
        void OnTransitionNameTextBoxLostFocus(object sender, RoutedEventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            textBox.ScrollToHome();
        }
 
        void OnTransitionNameTextBoxContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            // to disable the context menu
            e.Handled = true;
        }
 
        void OnCopyCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Selection selection = this.Context.Items.GetValue<Selection>();
            DesignerView designerView = this.Context.Services.GetService<DesignerView>();
 
            if (null != selection &&
                selection.SelectedObjects.Contains(this.ModelItem))
            {
                // Copy is intentionally disabled, when the root (TransitionDesigner)
                // is selected, because we don't support transition copy on the FreeFormPanel.
                e.CanExecute = false;
            }
            else
            {
                e.CanExecute = !this.IsReadOnly && CutCopyPasteHelper.CanCopy(this.Context);
            }
 
            e.ContinueRouting = false;
            e.Handled = true;
        }
 
        void OnCopyCommandExecute(object sender, ExecutedRoutedEventArgs e)
        {
            this.Context.Services.GetService<DesignerPerfEventProvider>().WorkflowDesignerCopyStart();
            CutCopyPasteHelper.DoCopy(this.Context);
            e.Handled = true;
            this.Context.Services.GetService<DesignerPerfEventProvider>().WorkflowDesignerCopyEnd();
        }
 
        private void OnWIPPreviewDragEnter(object sender, DragEventArgs e)
        {
            // We want to disable hover-to-expand for WIPs when dragging from the designer surface.
            // This is because after hover-to-expand, the transition designer will be unloaded. 
            // As a result, 1) there is no way to update other transitions if the shared trigger is updated, and 
            // 2) The ReorderableListEditor will be cleared and there is no way to update the source container
            // for actions if actions are updated.
 
            WorkflowItemPresenter presenter = (WorkflowItemPresenter)sender;
            if (presenter.Item != null && DragDropHelper.GetDraggedModelItems(e).Count<ModelItem>() > 0)
            {
                WorkflowViewElement topmostWFViewElement = this.FindTopmostWorkflowViewelementByHitTest(
                    presenter, e.GetPosition(presenter));
                bool isAlreadyExpanded = topmostWFViewElement != null ? topmostWFViewElement.ShowExpanded : false;
                if (!isAlreadyExpanded)
                {
                    // Handling the DragEnter would not only disable Auot-expand but also Auto-surround UI gesture (Bug 202880).
                    // To circumvent this problem, a new method (ShowSpacerHelperOnDraggedItems) is used to show
                    // the spacer directly.
                    presenter.ShowSpacerHelperOnDraggedItems(e);
                    e.Handled = true;
                }
            }
        }
 
        void OnAddNewTransitionLabelLoaded(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionLabel = (TextBlock)sender;
            this.addNewTransitionLabel.Visibility = Visibility.Collapsed;
        }
 
        void OnAddNewTransitionLabelUnloaded(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionLabel = null;
        }
        
        void OnNewTransitionTextBoxGotFocus(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionLabel.Visibility = Visibility.Visible;
            this.addNewTransitionBox.ViewModel.ComboBoxItems = new ObservableCollection<string>(
                GetAvailableStates().Select(
                modelItem => (modelItem.Properties[StateDesigner.DisplayNamePropertyName].ComputedValue as string)).Where(
                    displayName => !string.IsNullOrEmpty(displayName)).Distinct());
        }
 
        void OnNewTransitionCommitted(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionLabel.Visibility = Visibility.Collapsed;
 
            try
            {
                string selectedItem = this.NewTransition as string;
 
                if (null != selectedItem)
                {
                    AddNewTransition(selectedItem);
                }
 
                this.addNewTransitionBox.ResetText();
            }
            catch (ArgumentException ex)
            {
                ErrorReporting.ShowErrorMessage(ex.Message);
            }
        }
 
        void OnNewTransitionEditCancelled(object sender, RoutedEventArgs e)
        {
            this.addNewTransitionLabel.Visibility = Visibility.Collapsed;
        }
 
        private void AddNewTransition(string stateName)
        {
            ModelItem stateMachineModelItem = StateContainerEditor.GetStateMachineModelItem(this.parentStateModelItem);
            ModelItem toStateModelItem = null;
 
            foreach (ModelItem stateModelItem in stateMachineModelItem.Properties[StateMachineDesigner.StatesPropertyName].Collection)
            {
                if (string.Equals(stateName, stateModelItem.Properties[StateDesigner.DisplayNamePropertyName].ComputedValue as string, StringComparison.Ordinal))
                {
                    toStateModelItem = stateModelItem;
                }
            }
 
            if (null == toStateModelItem)
            {
                return;
            }
 
            Fx.Assert(toStateModelItem != null, "To state cannot be null.");
 
            using (EditingScope editingScope = (EditingScope)this.ModelItem.BeginEdit(SR.CreateTransition))
            {
                ModelItem triggerModelItem = this.ModelItem.Properties[TriggerPropertyName].Value;
                State toState = toStateModelItem.GetCurrentValue() as State;
 
                ModelItem newTransitionItem = this.parentStateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection.Add(new Transition()
                {
                    Trigger = null == triggerModelItem ? null : triggerModelItem.GetCurrentValue() as Activity,
                    DisplayName = StateContainerEditor.GenerateTransitionName(stateMachineModelItem),
                    To = toState
                });
 
                this.ViewStateService.StoreViewState(newTransitionItem, ExpandViewStateKey, true);
 
                if (null == triggerModelItem)
                {
                    PointCollection thisPointCollection = this.ViewStateService.RetrieveViewState(this.ModelItem, StateContainerEditor.ConnectorLocationViewStateKey) as PointCollection;
                    if (null != thisPointCollection && thisPointCollection.Any())
                    {
                        PointCollection newTransitionViewState = new PointCollection
                            {
                                thisPointCollection[0] // start point
                            };
 
                        if (toState == this.parentStateModelItem.GetCurrentValue())
                        {
                            // add an invalid destination point for self-transition, to force a reroute of the connection point
                            newTransitionViewState.Add(new Point(0, 0));
                        }
 
                        this.ViewStateService.StoreViewState(newTransitionItem, StateContainerEditor.ConnectorLocationViewStateKey, newTransitionViewState);
                    }
                }
 
                editingScope.Complete();
            }
 
            this.UpdateTransitionsSharingTrigger();
        }
 
        private IEnumerable<ModelItem> GetAvailableStates()
        {
            List<ModelItem> availableStates = new List<ModelItem>();
            ModelItem stateMachineModelItem = StateContainerEditor.GetStateMachineModelItem(this.parentStateModelItem);
            Fx.Assert(null != stateMachineModelItem, "StateMachine must be the ancestor.");
            Dictionary<ModelItem, int> stateToConnectionMap = new Dictionary<ModelItem, int>();
 
            foreach (ModelItem stateModelItem in stateMachineModelItem.Properties[StateMachineDesigner.StatesPropertyName].Collection)
            {
 
                if (!stateToConnectionMap.ContainsKey(stateModelItem))
                {
                    stateToConnectionMap[stateModelItem] = 0;
                }
 
                foreach (ModelItem transitionModelItem in stateModelItem.Properties[StateDesigner.TransitionsPropertyName].Collection)
                {
                    // to simplify the model, count a source connection as one, regardless of whether it is shared Trigger or not.
                    stateToConnectionMap[stateModelItem]++;
                    
                    ModelItem toStateModelItem = transitionModelItem.Properties[TransitionDesigner.ToPropertyName].Value;
                    Fx.Assert(toStateModelItem != null, "To state of a transition cannot be null.");
 
                    if (stateToConnectionMap.ContainsKey(toStateModelItem))
                    {
                        stateToConnectionMap[toStateModelItem]++;
                    }
                    else
                    {
                        stateToConnectionMap[toStateModelItem] = 1;
                    }
                }
            }
 
            foreach (ModelItem stateModelItem in stateToConnectionMap.Keys)
            {
                if (stateToConnectionMap[stateModelItem] < TotalFreeConnectionPointNum)
                {
                    // only allow connection to state that have available connection points
                    availableStates.Add(stateModelItem);
                }
            }
 
            return availableStates.OrderBy(modelItem => modelItem.Properties[StateDesigner.DisplayNamePropertyName].Value == null ? 
                SR.EmptyName : 
                modelItem.Properties[StateDesigner.DisplayNamePropertyName].Value.GetCurrentValue());
        }
 
        private WorkflowViewElement FindTopmostWorkflowViewelementByHitTest(Visual visualToHitTest, Point point)
        {
            HitTestResult result = VisualTreeHelper.HitTest(visualToHitTest, point);
            if (result == null)
            {
                return null;
            }
 
            for (DependencyObject obj = result.VisualHit;
                obj != null;
                obj = VisualTreeHelper.GetParent(obj))
            {
                if (obj is WorkflowViewElement)
                {
                    return (WorkflowViewElement)obj;
                }
            }
 
            return null;
        }
    }
}