1 //----------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 //----------------------------------------------------------------
4 namespace System.Activities.Presentation.Internal.PropertyEditing
7 using System.Collections.Generic;
8 using System.ComponentModel;
9 using System.Diagnostics;
10 using System.Diagnostics.CodeAnalysis;
11 using System.Globalization;
13 using System.Windows.Automation.Peers;
14 using System.Windows.Controls;
15 using System.Windows.Input;
16 using System.Windows.Threading;
18 using System.Activities.Presentation;
19 using System.Activities.Presentation.PropertyEditing;
21 using System.Activities.Presentation.Internal.PropertyEditing.Automation;
22 using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework;
23 using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework.Data;
24 using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework.PropertyInspector;
25 using System.Activities.Presentation.Internal.PropertyEditing.Model;
26 using System.Activities.Presentation.Internal.PropertyEditing.Selection;
27 using System.Activities.Presentation.Internal.PropertyEditing.State;
30 // Wrapper around ItemsControl that knows how to contain and display CategoryEntries,
31 // deals with AutomationPeers, and persists the open and closed state of individual
32 // CategoryContainers.
34 // This class should ideally be internal, but Avalon can't handle attached properties
35 // (which this class defines) on internal classes.
37 internal class CategoryList : ItemsControl, IEnumerable<CategoryBase>, IStateContainer, INotifyPropertyChanged
40 // This guy is static so that its values persist across designers and CategoryList instances
41 private static CategoryStateContainer _categoryStates = new CategoryStateContainer();
43 // This guy is not because it caches FilterString, which is specific to each CategoryList instance
44 private IStateContainer _stateContainer;
46 // Used for property selection
47 private FrameworkElement _selection;
48 private PropertySelectionMode _selectionMode;
50 // Used for property filtering
51 private string _filterString;
52 private PropertyFilter _currentFilter;
53 private bool _hasAnyFilterMatches = true;
54 private ICommand _clearFilterCommand;
57 private SharedPropertyValueColumnWidthContainer _sharedWidthContainer;
59 [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
62 // Make CategoryList non-focusable by default
63 UIElement.FocusableProperty.OverrideMetadata(typeof(CategoryList), new FrameworkPropertyMetadata(false));
65 // Mark the uber-CategoryList as the scope for property selection
66 PropertySelection.IsSelectionScopeProperty.OverrideMetadata(typeof(CategoryList), new PropertyMetadata(true));
72 [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
76 _stateContainer = new AggregateStateContainer(
77 new CategoryListStateContainer(this),
80 // Setup the shared width container
81 _sharedWidthContainer = new SharedPropertyValueColumnWidthContainer();
82 SharedPropertyValueColumnWidthContainer.SetOwningSharedPropertyValueColumnWidthContainer(this, _sharedWidthContainer);
84 // When someone new gets focus, we may need to mess around with selected property, so listen to the event
85 this.AddHandler(FocusManager.GotFocusEvent, new RoutedEventHandler(OnSomeoneGotFocus));
87 // When editing is done in the value editor, shift focus back to the selected property
88 this.CommandBindings.Add(new CommandBinding(PropertyValueEditorCommands.FinishEditing, OnFinishEditing));
90 // Need to call this method from a UI thread because some of Sparkle's value editors rely on it
91 UIThreadDispatcher.InitializeInstance();
95 // Event fired whenever a new CategoryContainer instance is generated
97 public event ContainerGeneratedHandler ContainerGenerated;
101 public event PropertyChangedEventHandler PropertyChanged;
106 return this.Items.Count;
111 // Gets or sets the filter string to apply to the shown properties.
112 // Setting this value will automatically apply the filter to the shown properties.
114 public string FilterString
117 return _filterString;
120 if (string.IsNullOrEmpty(value))
125 if (_filterString != value)
127 _filterString = value;
128 _currentFilter = new PropertyFilter(_filterString);
130 this.OnPropertyChanged("FilterString");
136 // Command-wrapper for the ClearFilter method
138 public ICommand ClearFilterCommand
141 if (_clearFilterCommand == null)
143 _clearFilterCommand = new DelegateCommand(this.ClearFilter);
145 return _clearFilterCommand;
150 // Gets a value indicating whether there are any categories or properties that
151 // match the current filter string
153 public bool HasAnyFilterMatches
156 return _hasAnyFilterMatches;
159 if (_hasAnyFilterMatches != value)
161 _hasAnyFilterMatches = value;
162 this.OnPropertyChanged("HasAnyFilterMatches");
168 // Gets the currently selected visual.
170 public FrameworkElement Selection
178 // Gets or sets the path to the currently selected item in the CategoryList.
179 // Only "sticky" selections will return a valid SelectionPath - otherwise
180 // null is returned. Setting this property changes the selection if the specified path
181 // can be resolved to a FrameworkElement to select and sets the SelectionMode to
184 public SelectionPath SelectionPath
187 if (_selection == null)
191 if (this.SelectionMode != PropertySelectionMode.Sticky)
196 ISelectionStop selectionStop = PropertySelection.GetSelectionStop(_selection);
197 if (selectionStop == null)
202 return selectionStop.Path;
205 SetSelectionPath(value);
210 // Gets or sets the current SelectionMode
212 private PropertySelectionMode SelectionMode
214 get { return _selectionMode; }
215 set { _selectionMode = value; }
218 public CategoryEntry this[int index] {
220 return (CategoryEntry)this.Items[index];
224 protected override AutomationPeer OnCreateAutomationPeer()
226 return new CategoryListAutomationPeer(this);
230 // Convenience Accessors
232 public void Insert(int index, CategoryEntry category)
234 if (category == null)
236 throw FxTrace.Exception.ArgumentNull("category");
239 this.Items.Insert(index, category);
242 public void InsertAlphabetically(CategoryEntry category)
245 // POSSIBLE OPTIMIZATION: optimize using the fact that the list of categories in this
246 // collection is ordered.
248 for (; index < this.Count; index++)
250 if (string.Compare(category.CategoryName, this[index].CategoryName, StringComparison.CurrentCulture) < 0)
256 this.Insert(index, category);
259 public void RemoveAt(int index)
261 this.Items.RemoveAt(index);
267 private void OnFinishEditing(object sender, ExecutedRoutedEventArgs e)
269 // Re-focus the selected selection stop
270 this.SynchronizeSelectionFocus(StealFocusMode.Always);
276 public void RestoreState(object state)
278 _stateContainer.RestoreState(state);
281 public object RetrieveState()
283 return _stateContainer.RetrieveState();
286 // This override both gets rid of the inbetween ContentPresenter and
287 // it makes the CategoryContainer available as an instance in PrepareContainerForItemOverride()
288 protected override DependencyObject GetContainerForItemOverride()
290 return new CiderCategoryContainer();
293 // Set the expansion state on the CategoryContainer based on an existing container
294 // or a cached value for the contained category
295 protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
297 CiderCategoryContainer container = element as CiderCategoryContainer;
298 CategoryEntry category = item as CategoryEntry;
300 if (container != null && category != null)
303 // Ideally, we would want to initalize the expansion state here. However,
304 // in collection editor, Blend messes around with the expansion state _after_
305 // the call to this method, which breaks our ability to remember which categories
306 // were open and which were closed. So, we set the state here (because Blend's
307 // search logic relies on it to be set by this point) and _reset_ it in the Loaded
310 // Look into stored state
311 CategoryState state = _categoryStates.GetCategoryState(category.CategoryName);
312 container.Expanded = state.CategoryExpanded;
313 container.AdvancedSectionPinned = state.AdvancedSectionExpanded;
315 // Hook into other event handlers that we care about (including the Loaded event)
316 container.Loaded += new RoutedEventHandler(OnContainerLoaded);
317 container.Unloaded += new RoutedEventHandler(OnContainerUnloaded);
319 if (ContainerGenerated != null)
321 ContainerGenerated(this, new ContainerGeneratedEventArgs(container));
326 Debug.Fail("CategoryList should only be populated with CategoryEntries.");
329 base.PrepareContainerForItemOverride(element, item);
332 // Re-initialize the expansion state here and hook into the ExpandedChanged and
333 // AdvancedSectionPinnedChanged events, because by now Blend may have ----ed those
334 // two values up (see comment in PrepareContainerForItemOverride() )
335 private void OnContainerLoaded(object sender, RoutedEventArgs e)
337 CiderCategoryContainer container = sender as CiderCategoryContainer;
339 if (container != null)
342 CategoryEntry category = container.Category;
343 if (category != null)
345 // Look into stored state
346 CategoryState state = _categoryStates.GetCategoryState(category.CategoryName);
347 container.Expanded = state.CategoryExpanded;
348 container.AdvancedSectionPinned = state.AdvancedSectionExpanded;
350 // Hook into these events here, because Blend won't mess the state up at this point
351 container.ExpandedChanged += new EventHandler(OnContainerExpandedChanged);
352 container.AdvancedSectionPinnedChanged += new EventHandler(OnAdvancedSectionPinnedChanged);
357 Debug.Fail("CategoryList expects the individual items to be CiderCategoryContainers");
361 private void OnContainerUnloaded(object sender, RoutedEventArgs e)
363 CiderCategoryContainer container = sender as CiderCategoryContainer;
364 if (container != null)
366 // Unhook from any events that we used to care about
367 container.ExpandedChanged -= new EventHandler(OnContainerExpandedChanged);
368 container.AdvancedSectionPinnedChanged -= new EventHandler(OnAdvancedSectionPinnedChanged);
369 container.Loaded -= new RoutedEventHandler(OnContainerLoaded);
370 container.Unloaded -= new RoutedEventHandler(OnContainerUnloaded);
374 Debug.Fail("Couldn't clean up event binding and store container state.");
378 private void OnContainerExpandedChanged(object sender, EventArgs e)
380 // If we are in "Filter-applied" mode, don't store the expansion state, since applying
381 // the filter automatically expands everything that matches that filter
382 if (_currentFilter != null && !_currentFilter.IsEmpty)
387 CiderCategoryContainer container = sender as CiderCategoryContainer;
388 if (container != null)
390 CategoryEntry category = container.Category;
391 if (category != null)
393 CategoryState state = _categoryStates.GetCategoryState(container.Category.CategoryName);
394 state.CategoryExpanded = container.Expanded;
399 private void OnAdvancedSectionPinnedChanged(object sender, EventArgs e)
401 // If we are in "Filter-applied" mode, don't store the expansion state, since applying
402 // the filter automatically expands everything that matches that filter
403 if (_currentFilter != null && !_currentFilter.IsEmpty)
408 CiderCategoryContainer container = sender as CiderCategoryContainer;
409 if (container != null)
411 CategoryEntry category = container.Category;
412 if (category != null)
414 CategoryState state = _categoryStates.GetCategoryState(container.Category.CategoryName);
415 state.AdvancedSectionExpanded = container.AdvancedSectionPinned;
423 // Applies the current filter to the existing list of categories and properties
424 // and updates the value of HasAnyFilterMatches. This class does not update itself
425 // automatically when new CategoryEntries are added or removed, so call this method
426 // explicitly when things change.
428 public void RefreshFilter()
430 bool? matchesFilter = null;
432 foreach (CategoryBase category in this.Items)
434 matchesFilter = matchesFilter == null ? false : matchesFilter;
435 matchesFilter |= ApplyFilter(_currentFilter, category);
438 this.HasAnyFilterMatches = matchesFilter == null ? true : (bool)matchesFilter;
442 // Clears the current property filter, if any
444 public void ClearFilter()
446 this.FilterString = null;
449 // Applies the specified filter to the specified category, returning a boolean indicating
450 // whether anything in that category matched the filter or not
451 private static bool ApplyFilter(PropertyFilter filter, CategoryBase category)
453 category.ApplyFilter(filter);
454 return category.MatchesFilter || category.BasicPropertyMatchesFilter || category.AdvancedPropertyMatchesFilter;
458 // Property Selection
461 // Sets the SelectionMode back to default. This is a common enough
462 // operation that it makes sense to abstract it to its own method.
464 private void ResetSelectionMode()
466 SelectionMode = PropertySelectionMode.Default;
470 // Updates property selection to the specified SelectionPath (if any)
471 // or the specified default property. Returns true if some property was selected,
472 // false otherwise (such as in the case when no properties are showing).
474 // <param name="stickyPath">SelectionPath to select. Takes precedence over default property.</param>
475 // <param name="defaultPropertyName">Property to select when no SelectionPath is specified or
476 // if the path cannot be resolved.</param>
477 // <param name="fallbackPath">SelectionPath to use when all else fails. May be null.</param>
478 // <returns>True if some property was selected, false otherwise (such as in the case
479 // when no properties are showing).</returns>
480 public bool UpdateSelectedProperty(
481 SelectionPath stickyPath,
482 string defaultPropertyName,
483 SelectionPath fallbackPath)
486 // First, try selecting the given stickyPath, if any
488 if (stickyPath == null || !SetSelectionPath(stickyPath))
490 ResetSelectionMode();
493 bool propertySelected;
495 if (SelectionMode == PropertySelectionMode.Default)
498 // Then, try finding and selecting the default property
500 propertySelected = defaultPropertyName == null ? false : SelectDefaultProperty(defaultPropertyName);
501 if (!propertySelected && fallbackPath != null)
504 // And if that fails, go to the specified fallback SelectionPath,
507 propertySelected = SetSelectionPath(fallbackPath);
510 // Make sure that we are still in Default selection mode
513 ResetSelectionMode();
517 propertySelected = true;
520 return propertySelected;
523 private bool SetSelectionPath(SelectionPath path)
525 // Dummy, this variable is only to satisfy SetSelectionPath(SelectionPath path, out bool pendingGeneration)
526 bool isPendingGenerationDummy;
527 return SetSelectionPath(path, out isPendingGenerationDummy);
530 // Attempts to resolve the specified path and set it as the current selection, returning
531 // true or false based on success. If the path is found, selection is set to Sticky.
532 // If the UI is not ready, return false and pendingGeneration is true.
533 internal bool SetSelectionPath(SelectionPath path, out bool pendingGeneration)
535 DependencyObject newSelection = SelectionPathResolver.ResolveSelectionPath(this, path, out pendingGeneration);
536 if (newSelection != null)
538 SelectAndFocus(newSelection, StealFocusMode.OnlyIfCategoryListHasFocusWithin);
539 this.SelectionMode = PropertySelectionMode.Sticky;
546 // When the user clicks somewhere, we try to find the closest parent with IsSelectionStop DP set to true and
549 protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
551 Select(e.OriginalSource as DependencyObject);
552 base.OnPreviewMouseDown(e);
555 // If we set Focus in OnMouseDown, it would be overwritten by the time the mouse went up. So, we set the focus
556 // in OnMouseUp() to make sure it sticks
558 protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
560 base.OnPreviewMouseUp(e);
561 SynchronizeSelectionFocus(StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin);
564 // When a UIElement gets focus, we try to find the parent PropertyContainer and make sure
567 private void OnSomeoneGotFocus(object source, RoutedEventArgs e)
569 Select(e.OriginalSource as DependencyObject);
572 // We only synchronize the IsSelected object with keyboard focus when the CategoryList itself
573 // has focus. If it doesn't we don't want to accidentally steal focus from somewhere else
574 // (say the design surface when the user is just tabbing through objects on it). However, once
575 // the CategoryList itself gains focus, we do want to synchronize the IsSelected object with
576 // keyboard focus so that Tabs and keyboard navigation works correctly.
578 protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e)
581 bool hasKeyboardFocus = (bool)e.NewValue;
583 if (hasKeyboardFocus)
585 // We just gained keyboard focus. Make sure we [....] up the current property selection
586 // with the keyboard focus, so that navigation works.
587 SynchronizeSelectionFocus(StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin);
590 base.OnIsKeyboardFocusWithinChanged(e);
593 // Helper method that ensures that the element marked as IsSelected also has focus.
594 // If it cannot receive focus, we look for its closest visual child that can
596 private void SynchronizeSelectionFocus(StealFocusMode focusMode)
598 // Is there something to select?
599 if (_selection == null)
604 // Are we even allowed to mess around with focus or does someone else on the
605 // design surface have focus right now and we should just let them have it?
606 if (focusMode == StealFocusMode.OnlyIfCategoryListHasFocusWithin && !this.IsKeyboardFocusWithin)
611 if (focusMode == StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin && _selection.IsKeyboardFocusWithin)
616 FrameworkElement focusableElement = VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection);
618 if (focusableElement != null)
620 focusableElement.Focus();
624 // Attempt to select the right thing for the specified default property name:
626 // * If there is a common DefaultProperty among selected objects AND there is a CategoryEditor consuming it,
627 // select the CategoryEditor
629 // * If there is a common DefaultProperty among selected objects AND there is NO CategoryEditor consuming it,
630 // select the property itself
632 // * If there is no common DefaultProperty, select the first common category
634 // * Otherwise fail by returning false
636 private bool SelectDefaultProperty(string defaultPropertyName)
638 return SelectDefaultPropertyHelper(defaultPropertyName, true);
641 private bool SelectDefaultPropertyHelper(string defaultPropertyName, bool firstTime)
643 if (string.IsNullOrEmpty(defaultPropertyName))
648 ModelCategoryEntry defaultPropertyCategory;
649 PropertyEntry defaultProperty = FindPropertyEntry(defaultPropertyName, out defaultPropertyCategory);
650 CategoryEditor defaultCategoryEditor = FindCategoryEditor(defaultProperty, defaultPropertyCategory);
651 UIElement element = null;
652 bool elementPendingGeneration = false;
654 // Try to look up the correct UIElement to select
656 if (defaultCategoryEditor != null)
658 element = FindCategoryEditorVisual(defaultCategoryEditor, defaultPropertyCategory, out elementPendingGeneration);
660 else if (defaultProperty != null)
662 element = FindPropertyEntryVisual(defaultProperty, defaultPropertyCategory, out elementPendingGeneration);
664 else if (this.Count > 0)
666 // Nothing found, so select the first selectable thing in the list
667 element = PropertySelection.FindNeighborSelectionStop<UIElement>(this, SearchDirection.Next);
668 elementPendingGeneration = false;
671 // If the UIElement was found, select it. Otherwise, if it should exist but it wasn't generated yet,
672 // wait until it is and try again.
677 SelectAndFocus(element, StealFocusMode.OnlyIfCategoryListHasFocusWithin);
679 // Ensure that we are in Default SelectionMode because calling SelectAndFocus automatically switches us
682 ResetSelectionMode();
685 else if (elementPendingGeneration && firstTime)
688 // Set the firstTime flag to false, to prevent any infinite loops should things go wrong
689 this.Dispatcher.BeginInvoke(
690 DispatcherPriority.Loaded,
691 new SelectDefaultPropertyHelperDelegate(SelectDefaultPropertyHelper),
696 else if (elementPendingGeneration && !firstTime)
698 elementPendingGeneration = false;
701 return element != null || elementPendingGeneration;
704 // Find the closest IsSelectable parent, select it and set focus on it.
706 private void SelectAndFocus(DependencyObject element, StealFocusMode focusMode)
709 SynchronizeSelectionFocus(focusMode);
712 // Find the closest IsSelectable parent and select it. Don't mess with focus.
714 private void Select(DependencyObject visualSource)
716 if (visualSource != null)
718 FrameworkElement selection = PropertySelection.FindParentSelectionStop<FrameworkElement>(visualSource);
720 if (selection != _selection)
723 // Unselect anything that was selected previously
724 if (_selection != null)
726 PropertySelection.SetIsSelected(_selection, false);
729 _selection = selection;
731 // Select whatever we need to select now
732 if (_selection != null)
734 PropertySelection.SetIsSelected(_selection, true);
736 // Bring the full PropertyContainer into view, if one exists
737 FrameworkElement focusableElement = VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection) ?? _selection;
738 FrameworkElement parentPropertyContainer = VisualTreeUtils.FindVisualAncestor<PropertyContainer>(focusableElement);
739 FrameworkElement bringIntoViewElement = parentPropertyContainer ?? focusableElement;
741 bringIntoViewElement.BringIntoView();
743 // As soon as the user manually selects a property, automatically switch to Sticky mode
744 _selectionMode = PropertySelectionMode.Sticky;
751 // Keyboard Navigation
753 protected override void OnKeyDown(KeyEventArgs e)
756 // Intercept Up, Down, Left, Right key strokes and use them to navigate around the
759 if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down)
761 if (_selection != null && !e.Handled)
764 Keyboard.FocusedElement,
765 VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection)))
768 ISelectionStop selectionStop = PropertySelection.GetSelectionStop(_selection);
769 if (selectionStop != null)
772 if (selectionStop.IsExpandable)
774 if ((e.Key == Key.Right && !selectionStop.IsExpanded) ||
775 (e.Key == Key.Left && selectionStop.IsExpanded))
778 selectionStop.IsExpanded = !selectionStop.IsExpanded;
788 SearchDirection direction = e.Key == Key.Up || e.Key == Key.Left ? SearchDirection.Previous : SearchDirection.Next;
789 FrameworkElement nextStop = PropertySelection.FindNeighborSelectionStop<FrameworkElement>(_selection, direction);
791 // If need to select something, select it
792 if (nextStop != null)
794 SelectAndFocus(nextStop, StealFocusMode.Always);
806 // SharedPropertyValueColumnWidth State Logic
808 protected override Size ArrangeOverride(Size arrangeBounds)
811 // Set the content width, rather than the entire container width
812 object gridLength = this.FindResource("OpenCloseColumnGridLength");
813 if (gridLength != null && gridLength is GridLength)
815 _sharedWidthContainer.ContainerWidth = arrangeBounds.Width - ((GridLength)gridLength).Value;
819 _sharedWidthContainer.ContainerWidth = arrangeBounds.Width;
822 return base.ArrangeOverride(arrangeBounds);
825 // Visual Lookup Helpers
828 // Looks for and returns the PropertyContainer used to represent the specified PropertyEntry. Returns
829 // null if not found.
831 // <param name="property">PropertyEntry to look up</param>
832 // <param name="parentCategory">Category to examine</param>
833 // <param name="pendingGeneration">Set to true if the specified property exists in a collapsed container
834 // (CategoryContainer or an advanced section). If so, the section is expanded, but the visual does not
835 // exist yet and should be requested later.
837 // <returns>PropertyContainer for the specified PropertyEntry if found, null otherwise</returns>
838 internal PropertyContainer FindPropertyEntryVisual(PropertyEntry property, CategoryEntry parentCategory, out bool pendingGeneration)
840 pendingGeneration = false;
842 if (property == null || parentCategory == null)
847 if (property.MatchesFilter == false)
852 CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(parentCategory) as CiderCategoryContainer;
853 if (categoryContainer == null)
858 // Expand the parent category, if it isn't already
859 if (!categoryContainer.Expanded)
861 categoryContainer.Expanded = true;
862 pendingGeneration = true;
865 // Expand the parent advanced section, if any and if it isn't already
866 if (property.IsAdvanced && !categoryContainer.AdvancedSectionPinned)
868 categoryContainer.AdvancedSectionPinned = true;
869 pendingGeneration = true;
872 bool pendingGenerationTemp;
873 PropertyContainer propertyContainer = categoryContainer.ContainerFromProperty(property, out pendingGenerationTemp);
874 pendingGeneration |= pendingGenerationTemp;
876 if (propertyContainer != null)
878 pendingGeneration = false;
881 return propertyContainer;
885 // Looks for and returns CategoryContainer for the specified CategoryEntry. Returns null if not
888 // <param name="category">CategoryEntry to look for.</param>
889 // <returns>Corresponding CategoryContainer if found, null otherwise.</returns>
890 internal CategoryContainer FindCategoryEntryVisual(CategoryEntry category)
892 if (category == null)
897 CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(category) as CiderCategoryContainer;
898 return categoryContainer;
902 // Looks for and returns the UIElement used to represent the specified CategoryEditor. Returns
903 // null if not found.
905 // <param name="editor">CategoryEditor to look for.</param>
906 // <param name="category">Category to look in.</param>
907 // <param name="pendingGeneration">Set to true if the specified editor exists in a collapsed container
908 // (CategoryContainer or an advanced section). If so, the section is expanded, but the visual does not
909 // exist yet and should be requested later.</param>
910 // <returns>UIElement for the specified CategoryEditor if found, null otherwise</returns>
911 internal UIElement FindCategoryEditorVisual(CategoryEditor editor, ModelCategoryEntry category, out bool pendingGeneration)
913 pendingGeneration = false;
915 if (editor == null || category == null)
920 UIElement editorVisual = null;
922 CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(category) as CiderCategoryContainer;
923 if (categoryContainer == null)
928 // Expand the parent category, if it isn't already
929 if (!categoryContainer.Expanded)
931 categoryContainer.Expanded = true;
932 pendingGeneration = true;
935 // Expand the parent advanced section, if any and if it isn't already
936 if (!categoryContainer.AdvancedSectionPinned && ExtensibilityAccessor.GetIsAdvanced(editor))
938 categoryContainer.AdvancedSectionPinned = true;
939 pendingGeneration = true;
942 bool pendingGenerationTemp;
943 editorVisual = categoryContainer.ContainerFromEditor(editor, out pendingGenerationTemp);
944 pendingGeneration |= pendingGenerationTemp;
946 if (editorVisual != null)
948 pendingGeneration = false;
954 // Logical Lookup Helpers
957 // Looks for a CategoryEntry contained in this list with the given name
959 // <param name="name">Name to look for</param>
960 // <returns>CategoryEntry with the specified name if found, null otherwise</returns>
961 internal CategoryEntry FindCategory(string name)
965 throw FxTrace.Exception.ArgumentNull("name");
968 foreach (CategoryEntry category in this.Items)
970 if (category.CategoryName.Equals(name))
980 // Looks for the PropertyEntry and its parent CategoryEntry for the specified property
982 // <param name="propertyName">Property to look for</param>
983 // <param name="parentCategory">Parent CategoryEntry of the given property</param>
984 // <returns>Corresponding PropertyEntry if found, null otherwise.</returns>
985 internal PropertyEntry FindPropertyEntry(string propertyName, out ModelCategoryEntry parentCategory)
987 parentCategory = null;
988 if (propertyName == null)
993 foreach (ModelCategoryEntry category in this.Items)
995 PropertyEntry property = category[propertyName];
996 if (property != null)
998 parentCategory = category;
1007 // Looks for the CategoryEditor of the specified type and returns it if found.
1009 // <param name="editorTypeName">Type name of the editor to look for.</param>
1010 // <param name="category">CategoryEntry that the editor belongs to, if found.</param>
1011 // <returns>CategoryEditor instance of the given type name if found, null otherwise.</returns>
1012 internal CategoryEditor FindCategoryEditor(string editorTypeName, out ModelCategoryEntry category)
1015 if (string.IsNullOrEmpty(editorTypeName) || this.Items == null)
1020 foreach (ModelCategoryEntry currentCategory in this.Items)
1022 foreach (CategoryEditor editor in currentCategory.CategoryEditors)
1024 if (string.Equals(editorTypeName, editor.GetType().Name))
1026 category = currentCategory;
1035 // Find the first CategoryEditor that consumes the specified property in the specified category.
1037 private static CategoryEditor FindCategoryEditor(PropertyEntry property, ModelCategoryEntry parentCategory)
1039 if (property == null || parentCategory == null)
1044 foreach (CategoryEditor editor in parentCategory.CategoryEditors)
1046 if (editor.ConsumesProperty(property))
1056 // IEnumerable<CategoryBase> Members
1058 public IEnumerator<CategoryBase> GetEnumerator()
1060 foreach (CategoryBase categoryBase in this.Items)
1062 yield return categoryBase;
1066 // IEnumerable Members
1068 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
1070 return this.GetEnumerator();
1074 // INotifyPropertyChanged Members
1076 private void OnPropertyChanged(string propertyName)
1078 if (PropertyChanged != null)
1080 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
1084 private delegate void MethodInvoker();
1086 // Delegate for SelectDefaultPropertyHelper method so that we can invoke it on Render priority if the
1087 // appropriate UIElements have not been generated yet
1089 private delegate bool SelectDefaultPropertyHelperDelegate(string defaultPropertyName, bool firstTime);
1091 // Enum that defines how and when we steal keyboard focus from the rest of the application
1092 // when the property selection changes
1094 private enum StealFocusMode
1097 OnlyIfCategoryListHasFocusWithin,
1098 OnlyIfCurrentSelectionDoesNotHaveFocusWithin
1101 // Manages state specific to this CategoryList instance
1102 private class CategoryListStateContainer : IStateContainer
1105 private CategoryList _parent;
1106 private IStateContainer _containers;
1108 public CategoryListStateContainer(CategoryList parent)
1112 throw FxTrace.Exception.ArgumentNull("parent");
1117 private IStateContainer Containers
1120 if (_containers == null)
1122 _containers = new AggregateStateContainer(
1123 new FilterStringStateContainer(_parent),
1124 new PropertyValueWidthStateContainer(_parent));
1131 public object RetrieveState()
1133 return Containers.RetrieveState();
1136 public void RestoreState(object state)
1138 Containers.RestoreState(state);
1141 // FilterStringStateContainer
1143 // StateContainer responsible for the FilterString
1145 private class FilterStringStateContainer : IStateContainer
1147 private CategoryList _parent;
1149 public FilterStringStateContainer(CategoryList parent)
1153 throw FxTrace.Exception.ArgumentNull("parent");
1158 public object RetrieveState()
1160 return _parent.FilterString;
1163 public void RestoreState(object state)
1165 string filterString = state as string;
1166 if (!string.IsNullOrEmpty(filterString))
1168 _parent.FilterString = filterString;
1173 // PropertyValueWidthStateContainer
1175 // StateContainer responsible for the width of the property value column
1177 private class PropertyValueWidthStateContainer : IStateContainer
1179 private CategoryList _parent;
1181 public PropertyValueWidthStateContainer(CategoryList parent)
1185 throw FxTrace.Exception.ArgumentNull("parent");
1190 public object RetrieveState()
1192 return _parent._sharedWidthContainer.ValueColumnPercentage.ToString(CultureInfo.InvariantCulture);
1195 public void RestoreState(object state)
1197 string stateString = state as string;
1198 if (stateString == null)
1204 if (!double.TryParse(stateString, NumberStyles.Float, CultureInfo.InvariantCulture, out percentage))
1206 Debug.Fail("Invalid PI state: " + stateString);
1210 if (percentage >= 0 && percentage <= 1)
1212 _parent._sharedWidthContainer.ValueColumnPercentage = percentage;
1216 Debug.Fail("Invalid percentage width stored in PI state: " + percentage.ToString(CultureInfo.InvariantCulture));