/// <summary> /// When overridden in a derived class, measures the size in layout required for child elements and determines a size for the FrameworkElement-derived /// class. /// </summary> /// <param name="constraint"> /// The available size that this element can give to child elements. Infinity can be specified as a value to indicate that the element will size to /// whatever content is available. /// </param> /// <returns>The size that this element determines it needs during layout, based on its calculations of child element sizes.</returns> protected override Size MeasureOverride(Size constraint) { // This keeps track of the space needed by this row. Size totalSize = new Size(); // Evaluate the size of all the columns in this row. Note that even though the cell may only want a fraction of the space available to it, we will // reserve all the space that the header has asked for (horizontally, that is). The standard version of this control tries to auto-calculate the width // of the column but it can only do that for the visible items. Since it is a virtualizing panel, there's no way to tell how wide each of the columns // needs to be without creating each one and measuring it. This would result in horrible performance. Therefore, it's not possible to automatically // calculate the width of a column without incurring a terrible performance penalty. This design simplifies the process of calculating the column width // by assuming the column have given us the same width for every column in the virtual display. if (this.Columns != null) { for (Int32 columnIndex = 0; columnIndex < this.Columns.Count; columnIndex++) { ColumnViewColumn columnViewColumn = this.Columns[columnIndex]; UIElement uiElement = this.Children[columnIndex]; Double remainingWidth = Math.Max(0.0, constraint.Width - totalSize.Width); Double width = columnIndex == 0 ? columnViewColumn.Width - DetailsViewRowPresenter.hangingIndent : columnViewColumn.Width; Size elementSize = new Size(Math.Min(remainingWidth, width), constraint.Height); uiElement.Measure(elementSize); totalSize.Width += elementSize.Width; totalSize.Height = Math.Max(totalSize.Height, uiElement.DesiredSize.Height); } } // This is the total space required for this row. return(totalSize); }
/// <summary> /// Handles a change to the MinWidth property. /// </summary> /// <param name="dependencyObject">The Object that originated the event.</param> /// <param name="dependencyPropertyChangedEventArgs">The event arguments.</param> static void OnMinWidthChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // Don't let the width exceed the minimum value. ColumnViewColumn columnViewColumn = dependencyObject as ColumnViewColumn; columnViewColumn.CoerceValue(ColumnViewColumn.WidthProperty); }
/// <summary> /// Handles a change to the Width property. /// </summary> /// <param name="dependencyObject">The owner of the property.</param> /// <param name="dependencyPropertyChangedEventArgs">The event arguments.</param> static void OnWidthChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // If the column width is set to Double.NaN then we need to auto size the column. Otherwise the column is given a specific width for all rows. ColumnViewColumn columnViewColumn = dependencyObject as ColumnViewColumn; columnViewColumn.OnPropertyChanged(WidthProperty.Name); }
/// <summary> /// Coerce the maximum value of the range. /// </summary> /// <param name="dependencyObject">The Object on which the property exists.</param> /// <param name="value">The new value of the property, prior to any coercion attempt.</param> /// <returns>The coerced value (with appropriate type).</returns> static Object CoerceWidth(DependencyObject dependencyObject, Object value) { // Don't let the width exceed the minimum value. ColumnViewColumn columnViewColumn = dependencyObject as ColumnViewColumn; return(Math.Max((Double)value, columnViewColumn.MinWidth)); }
/// <summary> /// Presents the user with a dialog box for managing the columns. /// </summary> /// <param name="sender">The object that originated the event.</param> /// <param name="e">The event data.</param> void OnMore(Object sender, ExecutedRoutedEventArgs e) { // This will extract the context menu that generated this event from the generic arguments. ContextMenu contextMenu = sender as ContextMenu; // From the context menu, we need the target that orignated the event and from that we can finally get the column set on which we want to operate. ColumnViewColumnHeader columnViewColumnHeader = contextMenu.PlacementTarget as ColumnViewColumnHeader; ListView listView = VisualTreeExtensions.FindAncestor <ListView>(columnViewColumnHeader); ColumnView columnView = listView.View as ColumnView; // This dialog box is used to add, remove, move or resize the columns in the view. It can't operate directly on the set of columns because those are // linked dynamically to the view, so we'll create a shallow clone of the values. ColumnViewChooseDetail columnViewChooseDetail = new ColumnViewChooseDetail(); foreach (ColumnViewColumn columnViewColumn in columnView.Columns) { columnViewChooseDetail.ListBox.Items.Add(new ColumnDescription(columnViewColumn)); } // Present the user with the chance to manage the columns. If the OK key is hit then copy the values out of the shallow copy and into the live column // set where they'll update the view. if (columnViewChooseDetail.ShowDialog() == true) { foreach (ColumnDescription columnDescription in columnViewChooseDetail.ListBox.Items) { ColumnViewColumn columnViewColumn = columnDescription.Column; columnViewColumn.IsVisible = columnDescription.IsVisible; columnViewColumn.Width = columnDescription.Width; } } }
/// <summary> /// Removes the column from the row. /// </summary> /// <param name="columnViewColumn">The ColumnViewColumn to be removed.</param> void RemoveCell(Int32 columnIndex) { // This will use the attached property to find the column to which this cell is bound through a weak listener. It will remove the listner (though this // isn't a requirement for the cell to be garbage collected) and finally remove the cell from the row. ColumnViewColumn columnViewColumn = this.Children[columnIndex].GetValue(ColumnViewRowPresenter.ColumnProperty) as ColumnViewColumn; PropertyChangedEventManager.RemoveListener(columnViewColumn, this, String.Empty); this.Children.RemoveAt(columnIndex); }
/// <summary> /// Installs the cell in the presenter. /// </summary> /// <param name="frameworkElement"></param> /// <param name="columnViewColumn"></param> void InstallCell(FrameworkElement frameworkElement, ColumnViewColumn columnViewColumn) { // Without the weak listener, the rows would not get updates when the column changes. If we went with a strong event listener reference, then the rows // could not be garbage collected. As the rows are managed by the ItemsGenerator, there is no chance to use an IDisposable interface on them, so the // weak event listener is the only chance we have of getting messages about changes to the columns. PropertyChangedEventManager.AddListener(columnViewColumn, this, String.Empty); // This allows the cell to find the cell which produced it and to which it is bound through a weak listner. Though not essential to remove the listener // when the element is removed from this row, it is good housekeeping. frameworkElement.SetValue(ColumnViewRowPresenter.ColumnProperty, columnViewColumn); }
/// <summary> /// Handles a change to the Column property. /// </summary> /// <param name="dependencyObject">The object that originated the event.</param> /// <param name="dependencyPropertyChangedEventArgs">The property change event arguments.</param> static void OnColumnPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // Extract the ColumnViewColumnHeader and the property from the generic event arguments. ColumnViewColumnHeader columnViewColumnHeader = dependencyObject as ColumnViewColumnHeader; ColumnViewColumn columnViewColumn = dependencyPropertyChangedEventArgs.NewValue as ColumnViewColumn; // This will force the sorting properties to reflect the sort order of the underlying column. columnViewColumnHeader.UpdateSortDirection(); // This will keep the header property 'HasFilters' reconciled to the same property in the column it represents. columnViewColumnHeader.SetValue(ColumnViewColumnHeader.hasFiltersPropertyKey, columnViewColumnHeader.Column.HasFilters); }
/// <summary> /// Handles a change to the column property. /// </summary> /// <param name="sender">The Object that originated the event.</param> /// <param name="propertyChangedEventArgs">The event arguments.</param> protected override void OnColumnPropertyChanged(Object sender, PropertyChangedEventArgs propertyChangedEventArgs) { // Validate the parameters. if (sender == null) { throw new ArgumentNullException("sender"); } if (propertyChangedEventArgs == null) { throw new ArgumentNullException("propertyChangedEventArgs"); } // This will get the column where the property change occurred from the sender. ColumnViewColumn columnViewColumn = sender as ColumnViewColumn; // If the column is visible, then it has a visual element which needs to have its properties reconciled with the ColumnViewColumn properties. if (columnViewColumn.IsVisible) { // This will find the visual element what has properites that must be reconciled with the header. Int32 columnIndex = this.Columns.IndexOf(columnViewColumn); ColumnViewColumnCell columnViewColumnCell = this.Children[columnIndex] as ColumnViewColumnCell; // This will apply the property in the column to the visual element representing that column in the current row. switch (propertyChangedEventArgs.PropertyName) { case "CellTemplate": case "CellTemplateSelector": case "DisplayMemberBinding": // Recreate the cell based on the new properties. this.Children.RemoveAt(columnIndex); FrameworkElement frameworkElement = ColumnViewRowPresenter.CreateCell(columnViewColumn); this.InstallCell(frameworkElement, columnViewColumn); this.Children.Insert(columnIndex, frameworkElement); break; case "Width": case "MinWidth": // Forcing a re-measurement of the row will pick up the changes made to the column's width. this.InvalidateMeasure(); break; } } }
/// <summary> /// Create a visual element based on the properties of the column. /// </summary> /// <param name="columnViewColumn">The column that defines the cell.</param> /// <returns>A visual element that holds the content of the cell.</returns> public static FrameworkElement CreateCell(ColumnViewColumn columnViewColumn) { // Validate the parameters. if (columnViewColumn == null) { throw new ArgumentNullException("columnViewColumn"); } // This will either create an element based on the templates associated with the ColumnViewColumn, or for simple binding, will create a simple cell and // bind it using the binding provided in the DisplayMemberBinding property. Both variations return a FrameworkElement that is used as an element in the // row to display the data associated with the column. FrameworkElement frameworkElement; if (columnViewColumn.DisplayMemberBinding == null) { // This will create a generic ContentPresenter using the templates associated with the ColumnView and bind the content of the generic control to // the data context (which contains the row data). It is the responsibility of the cell to select which property of the row data it wants to // display. It is important to remember that a ContentPresenter is not like other controls. The DataContext is not inheritied from the logical // parent, it is set to the Content property of the ContentPresenter. The main problem we're solving here is that the child control that's // generated from the ContentPresenter's template needs a DataContext, but the ContentPresenter doesn't have one since it's pointing to itself. To // solve this, we'll bind the Content of the ContentPresenter to the ColumnViewRowPresenter that is it's parent. This effectively restores the // DataContext inheritance that all other controls have. Clear as mud? ContentPresenter contentPresenter = new ContentPresenter(); contentPresenter.ContentTemplate = columnViewColumn.CellTemplate; contentPresenter.ContentTemplateSelector = columnViewColumn.CellTemplateSelector; Binding dataContextBinding = new Binding() { Path = new PropertyPath("DataContext") }; dataContextBinding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ColumnViewRowPresenter), 1); BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, dataContextBinding); frameworkElement = contentPresenter; } else { // This will create a cell to display the data provided by the 'DisplayMemberBinding' which selects which property of the row data is to be // displayed. ColumnViewColumnCell columnViewColumnCell = new ColumnViewColumnCell(); BindingOperations.SetBinding(columnViewColumnCell, ColumnViewColumnCell.ContentProperty, columnViewColumn.DisplayMemberBinding); frameworkElement = columnViewColumnCell; } // This element is used to present the data. return(frameworkElement); }
/// <summary> /// Handles a change to a generic property. /// </summary> /// <param name="dependencyObject">The Object that originated the event.</param> /// <param name="dependencyPropertyChangedEventArgs">The property change event arguments.</param> static void OnPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { // This will raise the INotifyPropertyChange event for any of the generic properties attached to this handler. ColumnViewColumn columnViewColumn = dependencyObject as ColumnViewColumn; columnViewColumn.OnPropertyChanged(dependencyPropertyChangedEventArgs.Property.Name); // This will provide a default value for the description when a string header is defined. The designer can always override it with an explicit // description of the column. The description shows up in the context menu and the 'Choose Column Details' dialog box. if (dependencyPropertyChangedEventArgs.Property == ColumnViewColumn.HeaderProperty) { String headerText = dependencyPropertyChangedEventArgs.NewValue as String; if (headerText != null && columnViewColumn.Description == null) { columnViewColumn.Description = headerText; } } }
/// <summary> /// Handles a change to the column collection associated with this presenter. /// </summary> /// <param name="notifyCollectionChangedEventArgs">The object that originated the event.</param> /// <param name="notifyCollectionChangedEventArgs">Provides data for the CollectionChanged event.</param> protected override void OnColumnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) { // The base class needs to adjust the column collection before we can operate on it. This will force the visual children that display the headers to // align with the column definitions. base.OnColumnCollectionChanged(sender, notifyCollectionChangedEventArgs); // This will cycle through the columns and set the 'IsFirst' property on the first visual header in the presenter. This property is used to give a // distinctive style to the first column, such as a hanging indent. if (this.Columns != null) { for (Int32 columnIndex = 0; columnIndex < this.Columns.Count; columnIndex++) { ColumnViewColumn columnViewColumn = this.Columns[columnIndex] as ColumnViewColumn; DetailsViewColumnHeader detailsViewColumnHeader = this.GetHeaderFromColumn(columnViewColumn) as DetailsViewColumnHeader; detailsViewColumnHeader.SetValue(DetailsViewColumnHeader.isFirstPropertyKey, columnIndex == 0); } } }
/// <summary> /// Adds a child object. /// </summary> /// <param name="value">The child object to add.</param> public void AddChild(Object value) { // Validate the parameters. if (value == null) { throw new ArgumentNullException("value"); } // Validate the parameter. ColumnViewColumn columnViewColumn = value as ColumnViewColumn; if (columnViewColumn == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "Illegal type of child {0}", value.GetType())); } // Add the column definition to the view. this.Columns.Add(columnViewColumn); }
/// <summary> /// Positions child elements and determines a size for a FrameworkElement derived class. /// </summary> /// <param name="arrangeSize">The final area within the parent that this element should use to arrange itself and its children.</param> /// <returns>The actual size used.</returns> protected override Size ArrangeOverride(Size arrangeSize) { // This will lay out all the element horizontally across the space of the RowPresenter using either the fixed width or the width of the item. Point location = new Point(); Double remainingWidth = arrangeSize.Width; // Evaluate the size of all the columns in this header. if (this.Columns != null) { for (Int32 columnIndex = 0; columnIndex < this.Columns.VisibleCount; columnIndex++) { ColumnViewColumn columnViewColumn = this.Columns[columnIndex]; UIElement uiElement = this.Children[columnIndex]; uiElement.Arrange(new Rect(location.X, 0.0, columnViewColumn.Width, arrangeSize.Height)); remainingWidth -= columnViewColumn.Width; location.X += columnViewColumn.Width; } } // This is the size of the row after laying everything out. return(arrangeSize); }
/// <summary> /// Create the visual element for the header. /// </summary> /// <param name="columnViewColumn">The ColumnViewColumn to be created.</param> /// <returns>The visual element representing the logical ColumnViewColumn.</returns> protected override ColumnViewColumnHeader CreateHeader(ColumnViewColumn columnViewColumn) { // This is the default column that is created for this presenter. Inheriting classes can override this for customer header. return(new DetailsViewColumnHeader()); }
/// <summary> /// Handles a change to the column collection. /// </summary> /// <param name="notifyCollectionChangedEventArgs">The Object that originated the event.</param> /// <param name="notifyCollectionChangedEventArgs">Provides data for the CollectionChanged event.</param> protected override void OnColumnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) { Int32 columnIndex; // Validate the parameters. if (sender == null) { throw new ArgumentNullException("sender"); } if (notifyCollectionChangedEventArgs == null) { throw new ArgumentNullException("notifyCollectionChangedEventArgs"); } // This will reconcile the visual elements in this control with the definition of the columns. switch (notifyCollectionChangedEventArgs.Action) { case NotifyCollectionChangedAction.Add: // Create a cell to hold the value and add each it to the row at the given index. columnIndex = notifyCollectionChangedEventArgs.NewStartingIndex == -1 ? 0 : notifyCollectionChangedEventArgs.NewStartingIndex; foreach (ColumnViewColumn columnViewColumn in notifyCollectionChangedEventArgs.NewItems) { if (columnViewColumn.IsVisible) { FrameworkElement frameworkElement = ColumnViewRowPresenter.CreateCell(columnViewColumn); this.InstallCell(frameworkElement, columnViewColumn); this.Children.Insert(columnIndex++, frameworkElement); } this.InvalidateMeasure(); } break; case NotifyCollectionChangedAction.Move: // Columns that appear after the visible count in the collection are hidden items. They exist only as a pool of potential columns. The items below // the 'VisibleCount' are actually visible in this view and appear in the same order in which they appear in this control. As items are moved in // and out of the visible range, we will create or destroy the visual elements used to show those columns in the viewer. Int32 oldIndex = notifyCollectionChangedEventArgs.OldStartingIndex; Int32 newIndex = notifyCollectionChangedEventArgs.NewStartingIndex; ColumnViewColumn movedColumn = this.Columns[newIndex]; if (this.Children.Count == this.Columns.VisibleCount) { // At this point, the collection hasn't changed. We are just moving columns around. UIElement uiElement = this.Children[oldIndex]; this.Children.RemoveAt(notifyCollectionChangedEventArgs.OldStartingIndex); this.Children.Insert(notifyCollectionChangedEventArgs.NewStartingIndex, uiElement); this.InvalidateArrange(); } else { // This will hide the column by destroying the visual elements used to display the column. if (oldIndex < this.Children.Count) { this.Children.RemoveAt(oldIndex); } // This will create a visual element to display the now visible column. if (movedColumn.IsVisible) { FrameworkElement frameworkElement = ColumnViewRowPresenter.CreateCell(movedColumn); this.InstallCell(frameworkElement, movedColumn); this.Children.Insert(newIndex, frameworkElement); } } break; case NotifyCollectionChangedAction.Remove: // Remove the offending cells from the specified location. columnIndex = notifyCollectionChangedEventArgs.OldStartingIndex; foreach (ColumnViewColumn columnViewColumn in notifyCollectionChangedEventArgs.OldItems) { this.RemoveCell(columnIndex++); } break; case NotifyCollectionChangedAction.Reset: // Clear them all. while (this.Children.Count != 0) { this.RemoveCell(0); } break; } }
/// <summary> /// Occurs when a particular instance of a context menu opens. /// </summary> /// <param name="sender">The object where the event handler is attached.</param> /// <param name="e">The event data.</param> void OnOpened(Object sender, RoutedEventArgs e) { // Extract the context menu from the generic event arguments. ContextMenu contextMenu = sender as ContextMenu; // We're going to clean out the context menu and re-evaluate the contents based on the context where the menu was opened. contextMenu.Items.Clear(); // This is the header that opened the context menu. From it, we need the ListView that hosts the ColumnView (that contains all the columns that we're // trying to manage with this context menu). ColumnViewColumnHeader columnViewColumnHeader = contextMenu.PlacementTarget as ColumnViewColumnHeader; ListView listView = VisualTreeExtensions.FindAncestor <ListView>(columnViewColumnHeader); ColumnView columnView = listView.View as ColumnView; // The task now is to fill in the menu items. We don't allow the user to resize a padding column, but will allow them to resize all the columns. if (columnViewColumnHeader.Role != ColumnViewColumnHeaderRole.Padding) { this.Items.Add(new MenuItem() { Command = Commands.FitColumn, CommandTarget = columnViewColumnHeader, Header = "Size Column to Fit" }); } this.Items.Add(new MenuItem() { Command = Commands.FitAllColumns, CommandTarget = listView, Header = "Size All Columns to Fit" }); this.Items.Add(new Separator()); // This will order the fixed menu items by their ordinal. An 'ordinal' is a numeric value that the designer can assign to the fixed menu items to // determine the order (or even if they'll appear) as a fixed menu item (that is, a quick way to add or remove a column, as opposed to selecting the // 'More...' button and using the list box to select an column). SortedList <Int32, ColumnViewColumn> commonColumns = new SortedList <Int32, ColumnViewColumn>(); foreach (ColumnViewColumn columnViewColumn in columnView.Columns) { if (columnViewColumn.Ordinal.HasValue && !commonColumns.ContainsKey(columnViewColumn.Ordinal.Value)) { commonColumns.Add(columnViewColumn.Ordinal.Value, columnViewColumn); } } // This will create a dedicated menu item for each of the columns that has been assigned an 'ordinal'. This allows the user to quickly add or remove // frequently used columns. Note that we attempt to format the column headers the way they would appear in the actual column header. foreach (KeyValuePair <Int32, ColumnViewColumn> keyValuePair in commonColumns) { ColumnViewColumn columnViewColumn = keyValuePair.Value; MenuItem menuItem = new MenuItem(); Binding isVisibleBinding = new Binding() { Path = new PropertyPath(ColumnViewColumn.IsVisibleProperty) }; isVisibleBinding.Source = columnViewColumn; BindingOperations.SetBinding(menuItem, MenuItem.IsCheckedProperty, isVisibleBinding); menuItem.Header = columnViewColumn.Description; menuItem.IsCheckable = true; this.Items.Add(menuItem); } // Place a separator in the context menu if there is a section above it with dedicated menu items for the column visibility settings. if (commonColumns.Count != 0) { this.Items.Add(new Separator()); } // Finally, a general purpose dialog box for managing the column set. this.Items.Add(new MenuItem() { Command = Commands.More, Header = "More..." }); }