|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Specialized; // NotifyCollectionChangedAction
using System.Windows.Controls.Primitives; // GridViewRowPresenterBase
using System.Windows.Data; // BindingBase
using MS.Internal; // DoubleUtil
namespace System.Windows.Controls
{
/// <summary>
/// An GridViewRowPresenter marks the site (in a style) of the panel that controls
/// layout of groups or items.
/// </summary>
public class GridViewRowPresenter : GridViewRowPresenterBase
{
//-------------------------------------------------------------------
//
// Public Methods
//
//-------------------------------------------------------------------
#region Public Methods
/// <summary>
/// Returns a string representation of this object.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return SR.Format(SR.ToStringFormatString_GridViewRowPresenter,
this.GetType(),
(Content != null) ? Content.ToString() : String.Empty,
(Columns != null) ? Columns.Count : 0);
}
#endregion
//-------------------------------------------------------------------
//
// Public Properties
//
//-------------------------------------------------------------------
#region Public Properties
/// <summary>
/// The DependencyProperty for the Content property.
/// Flags: None
/// Default Value: null
/// </summary>
// Any change in Content properties affectes layout measurement since
// a new template may be used. On measurement,
// ApplyTemplate will be invoked leading to possible application
// of a new template.
public static readonly DependencyProperty ContentProperty =
ContentControl.ContentProperty.AddOwner(
typeof(GridViewRowPresenter),
new FrameworkPropertyMetadata(
(object)null,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnContentChanged)));
/// <summary>
/// Content is the data used to generate the child elements of this control.
/// </summary>
public object Content
{
get { return GetValue(GridViewRowPresenter.ContentProperty); }
set { SetValue(GridViewRowPresenter.ContentProperty, value); }
}
/// <summary>
/// Called when ContentProperty is invalidated on "d."
/// </summary>
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GridViewRowPresenter gvrp = (GridViewRowPresenter)d;
//
// If the old and new value have the same type then we can save a lot of perf by
// keeping the existing ContentPresenters
//
Type oldType = (e.OldValue != null) ? e.OldValue.GetType() : null;
Type newType = (e.NewValue != null) ? e.NewValue.GetType() : null;
// DisconnectedItem doesn't count as a real type change
if (e.NewValue == BindingExpressionBase.DisconnectedItem)
{
gvrp._oldContentType = oldType;
newType = oldType;
}
else if (e.OldValue == BindingExpressionBase.DisconnectedItem)
{
oldType = gvrp._oldContentType;
}
if (oldType != newType)
{
gvrp.NeedUpdateVisualTree = true;
}
else
{
gvrp.UpdateCells();
}
}
#endregion
//-------------------------------------------------------------------
//
// Protected Methods
//
//-------------------------------------------------------------------
#region Protected Methods
/// <summary>
/// Override of <seealso cref="FrameworkElement.MeasureOverride" />.
/// </summary>
/// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
/// <returns>The GridViewRowPresenter's desired size.</returns>
protected override Size MeasureOverride(Size constraint)
{
GridViewColumnCollection columns = Columns;
if (columns == null) { return new Size(); }
UIElementCollection children = InternalChildren;
double maxHeight = 0.0; // Max height of children.
double accumulatedWidth = 0.0; // Total width consumed by children.
double constraintHeight = constraint.Height;
bool desiredWidthListEnsured = false;
foreach (GridViewColumn column in columns)
{
UIElement child = children[column.ActualIndex];
if (child == null) { continue; }
double childConstraintWidth = Math.Max(0.0, constraint.Width - accumulatedWidth);
if (column.State == ColumnMeasureState.Init
|| column.State == ColumnMeasureState.Headered)
{
if (!desiredWidthListEnsured)
{
EnsureDesiredWidthList();
LayoutUpdated += new EventHandler(OnLayoutUpdated);
desiredWidthListEnsured = true;
}
// Measure child.
child.Measure(new Size(childConstraintWidth, constraintHeight));
// As long as this is the first round of measure that has data participate
// the width should be ensured
// only element on current page paticipates in calculating the shared width
if (IsOnCurrentPage)
{
column.EnsureWidth(child.DesiredSize.Width);
}
DesiredWidthList[column.ActualIndex] = column.DesiredWidth;
accumulatedWidth += column.DesiredWidth;
}
else if (column.State == ColumnMeasureState.Data)
{
childConstraintWidth = Math.Min(childConstraintWidth, column.DesiredWidth);
child.Measure(new Size(childConstraintWidth, constraintHeight));
accumulatedWidth += column.DesiredWidth;
}
else // ColumnMeasureState.SpecificWidth
{
childConstraintWidth = Math.Min(childConstraintWidth, column.Width);
child.Measure(new Size(childConstraintWidth, constraintHeight));
accumulatedWidth += column.Width;
}
maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
}
// Reset this flag so that we will re-caculate it on every measure.
_isOnCurrentPageValid = false;
// reserve space for dummy header next to the last column
accumulatedWidth += c_PaddingHeaderMinWidth;
return (new Size(accumulatedWidth, maxHeight));
}
/// <summary>
/// GridViewRowPresenter computes the position of its children inside each child's Margin and calls Arrange
/// on each child.
/// </summary>
/// <param name="arrangeSize">Size the GridViewRowPresenter will assume.</param>
protected override Size ArrangeOverride(Size arrangeSize)
{
GridViewColumnCollection columns = Columns;
if (columns == null) { return arrangeSize; }
UIElementCollection children = InternalChildren;
double accumulatedWidth = 0.0;
double remainingWidth = arrangeSize.Width;
foreach (GridViewColumn column in columns)
{
UIElement child = children[column.ActualIndex];
if (child == null) { continue; }
// has a given value or 'auto'
double childArrangeWidth = Math.Min(remainingWidth, ((column.State == ColumnMeasureState.SpecificWidth) ? column.Width : column.DesiredWidth));
child.Arrange(new Rect(accumulatedWidth, 0, childArrangeWidth, arrangeSize.Height));
remainingWidth -= childArrangeWidth;
accumulatedWidth += childArrangeWidth;
}
return arrangeSize;
}
#endregion Protected Methods
//-------------------------------------------------------------------
//
// Internal Methods / Properties
//
//-------------------------------------------------------------------
#region Internal Methods / Properties
/// <summary>
/// Called when the Template's tree has been generated
/// </summary>
internal override void OnPreApplyTemplate()
{
// +-- GridViewRowPresenter ------------------------------------+
// | |
// | +- CtPstr1 ---+ +- CtPstr2 ---+ +- CtPstr3 ---+ |
// | | | | | | | ... |
// | +-------------+ +-------------+ +-------------+ |
// +------------------------------------------------------------+
base.OnPreApplyTemplate();
if (NeedUpdateVisualTree)
{
InternalChildren.Clear();
// build the whole collection from draft.
GridViewColumnCollection columns = Columns;
if (columns != null)
{
foreach (GridViewColumn column in columns.ColumnCollection)
{
InternalChildren.AddInternal(CreateCell(column));
}
}
NeedUpdateVisualTree = false;
}
// invalidate viewPort cache
_viewPortValid = false;
}
/// <summary>
/// Handler of column's PropertyChanged event. Update correspondent property
/// if change is of Width / CellTemplate / CellTemplateSelector.
/// </summary>
internal override void OnColumnPropertyChanged(GridViewColumn column, string propertyName)
{
Debug.Assert(column != null);
int index;
// ActualWidth change is a noise to RowPresenter, so filter it out.
// Note-on-perf: ActualWidth property change of will fire N x M times
// on every start up. (N: number of column with Width set to 'auto',
// M: number of visible items)
if (GridViewColumn.c_ActualWidthName.Equals(propertyName))
{
return;
}
// Width is the #1 property that will be changed frequently. The others
// (DisplayMemberBinding/CellTemplate/Selector) are not.
if (((index = column.ActualIndex) >= 0) && (index < InternalChildren.Count))
{
if (GridViewColumn.WidthProperty.Name.Equals(propertyName))
{
InvalidateMeasure();
}
// Priority: DisplayMemberBinding > CellTemplate > CellTemplateSelector
else if (GridViewColumn.c_DisplayMemberBindingName.Equals(propertyName))
{
FrameworkElement cell = InternalChildren[index] as FrameworkElement;
if (cell != null)
{
BindingBase binding = column.DisplayMemberBinding;
if (binding != null && cell is TextBlock)
{
cell.SetBinding(TextBlock.TextProperty, binding);
}
else
{
RenewCell(index, column);
}
}
}
else
{
ContentPresenter cp = InternalChildren[index] as ContentPresenter;
if (cp != null)
{
if (GridViewColumn.CellTemplateProperty.Name.Equals(propertyName))
{
DataTemplate dt;
if ((dt = column.CellTemplate) == null)
{
cp.ClearValue(ContentControl.ContentTemplateProperty);
}
else
{
cp.ContentTemplate = dt;
}
}
else if (GridViewColumn.CellTemplateSelectorProperty.Name.Equals(propertyName))
{
DataTemplateSelector dts;
if ((dts = column.CellTemplateSelector) == null)
{
cp.ClearValue(ContentControl.ContentTemplateSelectorProperty);
}
else
{
cp.ContentTemplateSelector = dts;
}
}
}
}
}
}
/// <summary>
/// process GridViewColumnCollection.CollectionChanged event.
/// </summary>
internal override void OnColumnCollectionChanged(GridViewColumnCollectionChangedEventArgs e)
{
base.OnColumnCollectionChanged(e);
if (e.Action == NotifyCollectionChangedAction.Move)
{
InvalidateArrange();
}
else
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// New child will always be appended to the very last, no matter it
// is actually add via 'Insert' or just 'Add'.
InternalChildren.AddInternal(CreateCell((GridViewColumn)(e.NewItems[0])));
break;
case NotifyCollectionChangedAction.Remove:
InternalChildren.RemoveAt(e.ActualIndex);
break;
case NotifyCollectionChangedAction.Replace:
InternalChildren.RemoveAt(e.ActualIndex);
InternalChildren.AddInternal(CreateCell((GridViewColumn)(e.NewItems[0])));
break;
case NotifyCollectionChangedAction.Reset:
InternalChildren.Clear();
break;
default:
break;
}
InvalidateMeasure();
}
}
// Used in UIAutomation
// Return the actual cells array (If user reorder column, the cell in InternalChildren isn't in the correct order)
internal List<UIElement> ActualCells
{
get
{
List<UIElement> list = new List<UIElement>();
GridViewColumnCollection columns = Columns;
if (columns != null)
{
UIElementCollection children = InternalChildren;
List<int> indexList = columns.IndexList;
if (children.Count == columns.Count)
{
for (int i = 0, count = columns.Count; i < count; ++i)
{
UIElement cell = children[indexList[i]];
if (cell != null)
{
list.Add(cell);
}
}
}
}
return list;
}
}
#endregion Internal Methods / Properties
//-------------------------------------------------------------------
//
// Private Methods
//
//-------------------------------------------------------------------
#region Private Methods
private void FindViewPort()
{
// assume GridViewRowPresenter is in Item's template
_viewItem = this.TemplatedParent as FrameworkElement;
if (_viewItem != null)
{
ItemsControl itemsControl = ItemsControl.ItemsControlFromItemContainer(_viewItem) as ItemsControl;
if (itemsControl != null)
{
ScrollViewer scrollViewer = itemsControl.ScrollHost as ScrollViewer;
if (scrollViewer != null)
{
// check if Virtualizing Panel do works
if (itemsControl.ItemsHost is VirtualizingPanel &&
scrollViewer.CanContentScroll)
{
// find the 'PART_ScrollContentPresenter' in GridViewScrollViewer
_viewPort = scrollViewer.GetTemplateChild(ScrollViewer.ScrollContentPresenterTemplateName) as FrameworkElement;
// in case GridViewScrollViewer is re-styled, say, cannot find PART_ScrollContentPresenter
if (_viewPort == null)
{
_viewPort = scrollViewer;
}
}
}
}
}
}
private bool CheckVisibleOnCurrentPage()
{
if (!_viewPortValid)
{
FindViewPort();
}
bool result = true;
if (_viewItem != null && _viewPort != null)
{
Rect viewPortBounds = new Rect(new Point(), _viewPort.RenderSize);
Rect itemBounds = new Rect(new Point(), _viewItem.RenderSize);
itemBounds = _viewItem.TransformToAncestor(_viewPort).TransformBounds(itemBounds);
// check if item bounds falls in view port bounds (in height)
result = CheckContains(viewPortBounds, itemBounds);
}
return result;
}
private bool CheckContains(Rect container, Rect element)
{
// Check if ANY part of the element reside in container
// return true if and only if (either case)
//
// +-------------------------------------------+
// + #================================# +
// +--#--------------------------------#-------+
// # #
// # #
// +--#--------------------------------#-------+
// + # # +
// +--#--------------------------------#-------+
// # #
// # #
// +--#--------------------------------#-------+
// + #================================# +
// +-------------------------------------------+
// The tolerance here is to make sure at least 2 pixels are inside container
const double tolerance = 2.0;
return ((CheckIsPointBetween(container, element.Top) && CheckIsPointBetween(container, element.Bottom)) ||
CheckIsPointBetween(element, container.Top + tolerance) ||
CheckIsPointBetween(element, container.Bottom - tolerance));
}
private bool CheckIsPointBetween(Rect rect, double pointY)
{
// return rect.Top <= pointY <= rect.Bottom
return (DoubleUtil.LessThanOrClose(rect.Top, pointY) &&
DoubleUtil.LessThanOrClose(pointY, rect.Bottom));
}
private void OnLayoutUpdated(object sender, EventArgs e)
{
bool desiredWidthChanged = false; // whether the shared minimum width has been changed since last layout
GridViewColumnCollection columns = Columns;
if(columns != null)
{
foreach (GridViewColumn column in columns)
{
if ((column.State != ColumnMeasureState.SpecificWidth))
{
column.State = ColumnMeasureState.Data;
if (DesiredWidthList == null || column.ActualIndex >= DesiredWidthList.Count)
{
// How can this happen?
// Between the last measure was called and this update is called, there can be a
// change done to the ColumnCollection and result in DesiredWidthList out of sync
// with the columnn collection. What can we do is end this call asap and the next
// measure will fix it.
desiredWidthChanged = true;
break;
}
if (!DoubleUtil.AreClose(column.DesiredWidth, DesiredWidthList[column.ActualIndex]))
{
// Update the record because collection operation latter on might
// need to verified this list again, e.g. insert an 'auto'
// column, so that we won't trigger unnecessary update due to
// inconsistency of this column.
DesiredWidthList[column.ActualIndex] = column.DesiredWidth;
desiredWidthChanged = true;
}
}
}
}
if (desiredWidthChanged)
{
InvalidateMeasure();
}
LayoutUpdated -= new EventHandler(OnLayoutUpdated);
}
private FrameworkElement CreateCell(GridViewColumn column)
{
Debug.Assert(column != null, "column shouldn't be null");
FrameworkElement cell;
BindingBase binding;
// Priority: DisplayMemberBinding > CellTemplate > CellTemplateSelector
if ((binding = column.DisplayMemberBinding) != null)
{
cell = new TextBlock();
// Needed this. Otherwise can't size to content at startup time.
// The reason is cell.Text is empty after the first round of measure.
cell.DataContext = Content;
cell.SetBinding(TextBlock.TextProperty, binding);
}
else
{
ContentPresenter cp = new ContentPresenter();
cp.Content = Content;
DataTemplate dt;
DataTemplateSelector dts;
if ((dt = column.CellTemplate) != null)
{
cp.ContentTemplate = dt;
}
if ((dts = column.CellTemplateSelector) != null)
{
cp.ContentTemplateSelector = dts;
}
cell = cp;
}
// copy alignment properties from ListViewItem
// for perf reason, not use binding here
ContentControl parent;
if ((parent = TemplatedParent as ContentControl) != null)
{
cell.VerticalAlignment = parent.VerticalContentAlignment;
cell.HorizontalAlignment = parent.HorizontalContentAlignment;
}
cell.Margin = _defalutCellMargin;
return cell;
}
private void RenewCell(int index, GridViewColumn column)
{
InternalChildren.RemoveAt(index);
InternalChildren.Insert(index, CreateCell(column));
}
/// <summary>
/// Updates all cells to the latest Content.
/// </summary>
private void UpdateCells()
{
ContentPresenter cellAsCP;
FrameworkElement cell;
UIElementCollection children = InternalChildren;
ContentControl parent = TemplatedParent as ContentControl;
for (int i = 0; i < children.Count; i++)
{
cell = (FrameworkElement)children[i];
if ((cellAsCP = cell as ContentPresenter) != null)
{
cellAsCP.Content = Content;
}
else
{
Debug.Assert(cell is TextBlock, "cells are either TextBlocks or ContentPresenters");
cell.DataContext = Content;
}
if (parent != null)
{
cell.VerticalAlignment = parent.VerticalContentAlignment;
cell.HorizontalAlignment = parent.HorizontalContentAlignment;
}
}
}
#endregion
//-------------------------------------------------------------------
//
// Private Properties / Fields
//
//-------------------------------------------------------------------
#region Private Properties / Fields
// if RowPresenter is not 'real' visible, it should not participating in measuring column width
// NOTE: IsVisible is force-inheriting parent's value, that's why we pick IsVisible instead of Visibility
// e.g. if RowPresenter's parent is hidden/collapsed (e.g. in ListTreeView),
// then RowPresenter.Visiblity = Visible, but RowPresenter.IsVisible = false
private bool IsOnCurrentPage
{
get
{
if (!_isOnCurrentPageValid)
{
_isOnCurrentPage = IsVisible && CheckVisibleOnCurrentPage();
_isOnCurrentPageValid = true;
}
return _isOnCurrentPage;
}
}
private FrameworkElement _viewPort;
private FrameworkElement _viewItem;
private Type _oldContentType;
private bool _viewPortValid = false;
private bool _isOnCurrentPage = false;
private bool _isOnCurrentPageValid = false;
private static readonly Thickness _defalutCellMargin = new Thickness(6, 0, 6, 0);
#endregion Private Properties / Fields
}
}
|