diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index 3e759b7..4ce6fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user *.userosscache @@ -19,6 +20,8 @@ [Rr]eleases/ x64/ x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ @@ -52,7 +55,6 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json # StyleCop StyleCopReport.xml @@ -60,7 +62,7 @@ StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c -*_i.h +*_h.h *.ilk *.meta *.obj @@ -77,6 +79,7 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj +*_wpftmp.csproj *.log *.vspscc *.vssscc @@ -208,7 +211,7 @@ _pkginfo.txt # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!*.[Cc]ache/ +!?*.[Cc]ache/ # Others ClientBin/ @@ -221,7 +224,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -252,6 +255,7 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser +*- Backup*.rdl # Microsoft Fakes FakesAssemblies/ @@ -291,8 +295,8 @@ paket-files/ .idea/ *.sln.iml -# CodeRush -.cr/ +# CodeRush personal settings +.cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ @@ -317,7 +321,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -326,5 +330,11 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder .mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/.mergify.yml b/.mergify.yml index 20d1f1b..2432f7a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,18 +1,18 @@ pull_request_rules: - - name: automatic strict merge when CI passes, has 2 reviews, no requests for change and is labeled 'ready-to-merge' unless labelled 'do-not-merge/breaking-change' or 'do-not-merge/work-in-progress' + - name: automatic strict merge when CI passes, has 1 reviews, no requests for change and is labeled 'ready-to-merge' unless labelled 'do-not-merge/breaking-change' or 'do-not-merge/work-in-progress' conditions: # Only pull-requests sent to the master branch - base=master # All Azure builds should be green: - - status-success=Uno.UI - CI + - status-success=Uno.WinUI3Convert - CI # CLA check must pass: #- "status-success=license/cla" # Note that this only matches people with write / admin access to the repo, # see - - "#approved-reviews-by>=2" + - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" # Pull-request must be labeled with: @@ -30,10 +30,3 @@ pull_request_rules: # https://doc.mergify.io/strict-workflow.html # https://doc.mergify.io/actions.html#label strict: smart - - - name: automatic merge for allcontributors pull requests - conditions: - - author=allcontributors[bot] - actions: - merge: - method: merge diff --git a/README.md b/README.md index bcf37f2..8ae1dc2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# template -template for brand new github repositories +# Uno.WinUI3Convert + +Migrate UWP projects to WinUI3/NET5. + +This tool is commonly used in CI environments to automatically generate a WinUI 3 compatible source tree, built separately from the UWP source tree. This allows for the generation of WinUI 3 compatible nuget packages for libraries without having to maintain two separate codebases. + +``` +Usage: + winui3convert [options] + +Arguments: + Source directory + Destination directory + +Options: + --overwrite Overwrite destination + --version Show version information + -?, -h, --help Show help and usage information +``` + +## Installation + +dotnet tool install --global uno.winui3convert + +## Conversion adjustments + +This tool is meant to help migrate your projects by rewriting namespaces and project files. It won't resolve collisions, work around unsupported features or change code in significant ways. + +Manual source adjustments are to be expected in some cases. \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionView.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionView.cs new file mode 100644 index 0000000..f88378e --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionView.cs @@ -0,0 +1,1249 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + internal abstract class CollectionView : ICollectionView, INotifyCollectionChanged, INotifyPropertyChanged + { + /// + /// Raise this event before changing currency. + /// + public virtual event CurrentChangingEventHandler CurrentChanging; + + /// + /// Raise this event after changing currency. + /// + public virtual event EventHandler CurrentChanged; + + /// + /// Unused VectorChanged event from the ICollectionView interface. + /// + event VectorChangedEventHandler IObservableVector.VectorChanged + { + add + { + throw new NotImplementedException(); + } + + remove + { + throw new NotImplementedException(); + } + } + + public CollectionView(IEnumerable collection) + { + _sourceCollection = collection ?? throw new ArgumentNullException("collection"); + + // forward collection change events from underlying collection to our listeners. + INotifyCollectionChanged incc = collection as INotifyCollectionChanged; + if (incc != null) + { + _sourceWeakEventListener = + new WeakEventListener(this) + { + // Call the actual collection changed event + OnEventAction = (source, changed, arg) => OnCollectionChanged(source, arg), + + // The source doesn't exist anymore + OnDetachAction = (listener) => incc.CollectionChanged -= _sourceWeakEventListener.OnEvent + }; + incc.CollectionChanged += _sourceWeakEventListener.OnEvent; + } + + _currentItem = null; + _currentPosition = -1; + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, _currentPosition < 0); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, _currentPosition < 0); + SetFlag(CollectionViewFlags.CachedIsEmpty, _currentPosition < 0); + } + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Gets or sets a callback used to determine if an item is suitable for inclusion in the view. + /// + /// + /// Simpler implementations do not support filtering and will throw a NotSupportedException. + /// Use property to test if filtering is supported before + /// assigning a non-null value. + /// + public virtual Predicate Filter + { + get + { + return _filter; + } + + set + { + if (!CanFilter) + { + throw new NotSupportedException(); + } + + _filter = value; + + RefreshOrDefer(); + } + } + + /// + /// Gets a value indicating whether or not this ICollectionView can do any filtering. + /// When false, set will throw an exception. + /// + public abstract bool CanFilter + { + get; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Collection of Sort criteria to sort items in this view over the SourceCollection. + /// + /// + ///

+ /// Simpler implementations do not support sorting and will return an empty + /// and immutable / read-only SortDescription collection. + /// Attempting to modify such a collection will cause NotSupportedException. + /// Use property on CollectionView to test if sorting is supported + /// before modifying the returned collection. + ///

+ ///

+ /// One or more sort criteria in form of SortDescription can be added, each specifying a property and direction to sort by. + ///

+ ///
+ public abstract SortDescriptionCollection SortDescriptions + { + get; + } + + /// + /// Gets a value indicating whether this ICollectionView supports sorting. + /// + public abstract bool CanSort + { + get; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_GROUP + /// + /// Gets a value indicating whether this view really supports grouping. + /// When this returns false, the rest of the interface is ignored. + /// + public abstract bool CanGroup + { + get; + } + + /// + /// Gets the description of grouping, indexed by level. + /// + public abstract ObservableCollection GroupDescriptions + { + get; + } + + /// + /// Gets the top-level groups, constructed according to the descriptions + /// given in GroupDescriptions. + /// + public abstract ReadOnlyObservableCollection Groups + { + get; + } +#endif + + public virtual IObservableVector CollectionGroups + { + get + { + return null; + } + } + + /// + /// Gets or sets the Culture to use during sorting. + /// + public virtual CultureInfo Culture + { + get + { + return _culture; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + if (_culture != value) + { + _culture = value; + OnPropertyChanged(CulturePropertyName); + } + } + } + + /// + /// Enter a Defer Cycle. + /// Defer cycles are used to coalesce changes to the ICollectionView. + /// + /// An IDisposable deferral object. + public virtual IDisposable DeferRefresh() + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView ecv = this as IEditableCollectionView; + if (ecv != null && (ecv.IsAddingNew || ecv.IsEditingItem)) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("DeferRefresh"); + } +#endif + _deferLevel++; + return new DeferHelper(this); + } + + /// + /// Gets the "current item" for this view + /// + /// + /// Only wrapper classes (those that pass currency handling calls to another internal + /// CollectionView) should override CurrentItem; all other derived classes + /// should use SetCurrent() to update the current values stored in the base class. + /// + public virtual object CurrentItem + { + get + { + VerifyRefreshNotDeferred(); + + return _currentItem; + } + } + + /// + /// Gets the ordinal position of the within the (optionally + /// sorted and filtered) view. + /// + /// + /// -1 if the CurrentPosition is unknown, because the collection does not have an + /// effective notion of indices, or because CurrentPosition is being forcibly changed + /// due to a CollectionChange. + /// + /// + /// Only wrapper classes (those that pass currency handling calls to another internal + /// CollectionView) should override CurrenPosition; all other derived classes + /// should use SetCurrent() to update the current values stored in the base class. + /// + public virtual int CurrentPosition + { + get + { + VerifyRefreshNotDeferred(); + + return _currentPosition; + } + } + + /// + /// Gets a value indicating whether the source has more items + /// + public bool HasMoreItems + { + get + { + ISupportIncrementalLoading sourceAsSupportIncrementalLoading = _sourceCollection as ISupportIncrementalLoading; + return sourceAsSupportIncrementalLoading?.HasMoreItems ?? false; + } + } + + /// + /// Gets a value indicating whether is beyond the end (End-Of-File). + /// + public virtual bool IsCurrentAfterLast + { + get + { + VerifyRefreshNotDeferred(); + + return CheckFlag(CollectionViewFlags.IsCurrentAfterLast); + } + } + + /// + /// Gets a value indicating whether is before the beginning (Beginning-Of-File). + /// + public virtual bool IsCurrentBeforeFirst + { + get + { + VerifyRefreshNotDeferred(); + + return CheckFlag(CollectionViewFlags.IsCurrentBeforeFirst); + } + } + + public bool IsReadOnly + { + get + { + return true; + } + } + + /// + /// Gets the underlying collection. + /// + public virtual IEnumerable SourceCollection + { + get + { + return _sourceCollection; + } + } + + public object this[int index] + { + get + { + return GetItemAt(index); + } + + set + { + throw new NotImplementedException(); + } + } + + public void Add(object item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + /// + /// Return true if the item belongs to this view. No assumptions are + /// made about the item. This method will behave similarly to IList.Contains(). + /// + /// + ///

If the caller knows that the item belongs to the + /// underlying collection, it is more efficient to call PassesFilter. + /// If the underlying collection is only of type IEnumerable, this method + /// is a O(N) operation

+ ///
+ /// True if the item belongs to this view. + public abstract bool Contains(object item); + + public void CopyTo(object[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public void Insert(int index, object item) + { + throw new NotImplementedException(); + } + + /// + /// Invoked to load more items from the source. + /// + /// number of items to load + /// Async operation of LoadMoreItemsResult + public IAsyncOperation LoadMoreItemsAsync(uint count) + { + ISupportIncrementalLoading sourceAsSupportIncrementalLoading = _sourceCollection as ISupportIncrementalLoading; + return sourceAsSupportIncrementalLoading?.LoadMoreItemsAsync(count); + } + + /// + /// Move to the first item. + /// + /// True if points to an item within the view. + public virtual bool MoveCurrentToFirst() + { + VerifyRefreshNotDeferred(); + + return MoveCurrentToPosition(0); + } + + /// + /// Move to the last item. + /// + /// True if points to an item within the view. + public virtual bool MoveCurrentToLast() + { + VerifyRefreshNotDeferred(); + + return MoveCurrentToPosition(Count - 1); + } + + /// + /// Move to the next item. + /// + /// True if points to an item within the view. + public virtual bool MoveCurrentToNext() + { + VerifyRefreshNotDeferred(); + + if (CurrentPosition < Count) + { + return MoveCurrentToPosition(CurrentPosition + 1); + } + else + { + return false; + } + } + + /// + /// Move to the previous item. + /// + /// True if points to an item within the view. + public virtual bool MoveCurrentToPrevious() + { + VerifyRefreshNotDeferred(); + + if (CurrentPosition >= 0) + { + return MoveCurrentToPosition(CurrentPosition - 1); + } + else + { + return false; + } + } + + /// + /// Move to the given item. + /// If the item is not found, move to BeforeFirst. + /// + /// Move CurrentItem to this item. + /// True if points to an item within the view. + public virtual bool MoveCurrentTo(object item) + { + VerifyRefreshNotDeferred(); + + // if already on item, don't do anything + if (object.Equals(CurrentItem, item)) + { + // also check that we're not fooled by a false null _currentItem + if (item != null || IsCurrentInView) + { + return IsCurrentInView; + } + } + + int index = -1; +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView ecv = this as IEditableCollectionView; + bool isNewItem = ecv != null && ecv.IsAddingNew && object.Equals(item, ecv.CurrentAddItem); +#elif FEATURE_ICOLLECTIONVIEW_FILTER + bool isNewItem = false; +#endif + +#if FEATURE_ICOLLECTIONVIEW_FILTER + if (isNewItem || item == null || PassesFilter(item)) +#endif + { + // if the item is not found IndexOf() will return -1, and + // the MoveCurrentToPosition() below will move current to BeforeFirst + index = IndexOf(item); + } + + return MoveCurrentToPosition(index); + } + + /// + /// Move to the item at the given index. + /// + /// Move CurrentItem to this index + /// True if points to an item within the view. + public abstract bool MoveCurrentToPosition(int position); + + /// + /// Re-create the view, using any SortDescriptions and/or Filter. + /// + public virtual void Refresh() + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView ecv = this as IEditableCollectionView; + if (ecv != null && (ecv.IsAddingNew || ecv.IsEditingItem)) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("Refresh"); + } +#endif + RefreshInternal(); + } + + public bool Remove(object item) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotImplementedException(); + } + + /// + /// Returns an object that enumerates the items in this view. + /// + /// IEnumerator object that enumerates the items in this view. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Return true if the item belongs to this view. The item is assumed to belong to the + /// underlying DataCollection; this method merely takes filters into account. + /// It is commonly used during collection-changed notifications to determine if the added/removed + /// item requires processing. + /// Returns true if no filter is set on collection view. + /// + /// True if the item belongs to this view. + public abstract bool PassesFilter(object item); +#endif + + /// + /// Return the index where the given item belongs, or -1 if this index is unknown. + /// + /// + /// If this method returns an index other than -1, it must always be true that + /// view[index-1] < item <= view[index], where the comparisons are done via + /// the view's IComparer.Compare method (if any). + /// (This method is used by a listener's (e.g. System.Windows.Controls.ItemsControl) + /// CollectionChanged event handler to speed up its reaction to insertion and deletion of items. + /// If IndexOf is not implemented, a listener does a binary search using IComparer.Compare.) + /// + /// data item + /// The index where the given item belongs, or -1 if this index is unknown. + public abstract int IndexOf(object item); + + /// + /// Retrieve item at the given zero-based index in this CollectionView. + /// + /// + ///

The index is evaluated with any SortDescriptions or Filter being set on this CollectionView. + /// If the underlying collection is only of type IEnumerable, this method + /// is a O(N) operation.

+ ///

When deriving from CollectionView, override this method to provide + /// a more efficient implementation.

+ ///
+ /// + /// Thrown if index is out of range + /// + /// Item at the given zero-based index in this CollectionView. + public abstract object GetItemAt(int index); + + /// + /// Gets the number of items (or -1, meaning "don't know"); + /// if a Filter is set, this counts only items that pass the filter. + /// + /// + ///

If the underlying collection is only of type IEnumerable, this count + /// is a O(N) operation; this Count value will be cached until the + /// collection changes again.

+ ///

When deriving from CollectionView, override this property to provide + /// a more efficient implementation.

+ ///
+ public abstract int Count + { + get; + } + + /// + /// Gets a value indicating whether the resulting (filtered) view is empty. + /// + public abstract bool IsEmpty + { + get; + } + + /// + /// Gets an object that compares items in this view. + /// + public virtual IComparer Comparer + { + get + { + return this as IComparer; + } + } + + /// + /// Gets a value indicating whether this view needs to be refreshed. + /// + public virtual bool NeedsRefresh + { + get + { + return CheckFlag(CollectionViewFlags.NeedsRefresh); + } + } + + /// + /// Raise this event when the (filtered) view changes + /// + public virtual event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + /// Raises a PropertyChanged event (per ). + /// + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + /// + /// PropertyChanged event (per ). + /// + public virtual event PropertyChangedEventHandler PropertyChanged; + + /// + /// Re-create the view, using any SortDescriptions and/or Filter. + /// + protected abstract void RefreshOverride(); + + /// + /// Returns an object that enumerates the items in this view. + /// + /// An object that enumerates the items in this view. + protected abstract IEnumerator GetEnumerator(); + + /// + /// Notify listeners that this View has changed + /// + /// + /// CollectionViews (and sub-classes) should take their filter/sort/grouping + /// into account before calling this method to forward CollectionChanged events. + /// + /// + /// The NotifyCollectionChangedEventArgs to be passed to the EventHandler + /// + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException("args"); + } + + unchecked + { + // invalidate enumerators because of a change + ++_timestamp; + } + + CollectionChanged?.Invoke(this, args); + + // Collection changes change the count unless an item is being + // replaced or moved within the collection. + if (args.Action != NotifyCollectionChangedAction.Replace) + { + OnPropertyChanged(CountPropertyName); + } + + bool isEmpty = IsEmpty; + if (isEmpty != CheckFlag(CollectionViewFlags.CachedIsEmpty)) + { + SetFlag(CollectionViewFlags.CachedIsEmpty, isEmpty); + OnPropertyChanged(IsEmptyPropertyName); + } + } + + /// + /// set CurrentItem and CurrentPosition, no questions asked! + /// + /// + /// CollectionViews (and sub-classes) should use this method to update + /// the Current__ values. + /// + protected void SetCurrent(object newItem, int newPosition) + { + int count = (newItem != null) ? 0 : IsEmpty ? 0 : Count; + SetCurrent(newItem, newPosition, count); + } + + /// + /// set CurrentItem and CurrentPosition, no questions asked! + /// + /// + /// This method can be called from a constructor - it does not call + /// any virtuals. The 'count' parameter is substitute for the real Count, + /// used only when newItem is null. + /// In that case, this method sets IsCurrentAfterLast to true if and only + /// if newPosition >= count. This distinguishes between a null belonging + /// to the view and the dummy null when CurrentPosition is past the end. + /// + protected void SetCurrent(object newItem, int newPosition, int count) + { + if (newItem != null) + { + // non-null item implies position is within range. + // We ignore count - it's just a placeholder + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, false); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, false); + } + else if (count == 0) + { + // empty collection - by convention both flags are true and position is -1 + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, true); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, true); + newPosition = -1; + } + else + { + // null item, possibly within range. + SetFlag(CollectionViewFlags.IsCurrentBeforeFirst, newPosition < 0); + SetFlag(CollectionViewFlags.IsCurrentAfterLast, newPosition >= count); + } + + _currentItem = newItem; + _currentPosition = newPosition; + } + + /// + /// Ask listeners (via event) if it's OK to change currency + /// + /// false if a listener cancels the change, true otherwise + protected bool OKToChangeCurrent() + { + CurrentChangingEventArgs args = new CurrentChangingEventArgs(); + OnCurrentChanging(args); + return !args.Cancel; + } + + /// + /// Raise a CurrentChanging event that is not cancelable. + /// Internally, CurrentPosition is set to -1. + /// This is called by CollectionChanges (Remove and Refresh) that affect the CurrentItem. + /// + /// + /// This CurrentChanging event cannot be canceled. + /// + protected void OnCurrentChanging() + { + _currentPosition = -1; + OnCurrentChanging(UncancelableCurrentChangingEventArgs); + } + + /// + /// Raises the CurrentChanging event + /// + /// + /// CancelEventArgs used by the consumer of the event. args.Cancel will + /// be true after this call if the CurrentItem should not be changed for + /// any reason. + /// + /// + /// This CurrentChanging event cannot be canceled. + /// + protected virtual void OnCurrentChanging(CurrentChangingEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException("args"); + } + + if (_currentChangedMonitor.Busy) + { + if (args.IsCancelable) + { + args.Cancel = true; + } + + return; + } + + CurrentChanging?.Invoke(this, args); + } + + /// + /// Raises the CurrentChanged event + /// + protected virtual void OnCurrentChanged() + { + if (CurrentChanged != null && _currentChangedMonitor.Enter()) + { + using (_currentChangedMonitor) + { + CurrentChanged(this, EventArgs.Empty); + } + } + } + + /// + /// Must be implemented by the derived classes to process a single change on the + /// UI thread. The UI thread will have already been entered by now. + /// + /// + /// The NotifyCollectionChangedEventArgs to be processed. + /// + protected abstract void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args); + + /// + /// Handle CollectionChanged events. + /// Calls ProcessCollectionChanged() or posts the change to the Dispatcher to process on the correct thread. + /// + /// + /// User should override + /// + protected void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (CheckFlag(CollectionViewFlags.ShouldProcessCollectionChanged)) + { + ProcessCollectionChanged(args); + } + } + + /// + /// Refresh, or mark that refresh is needed when defer cycle completes. + /// + protected void RefreshOrDefer() + { + if (IsRefreshDeferred) + { + SetFlag(CollectionViewFlags.NeedsRefresh, true); + } + else + { + RefreshInternal(); + } + } + + //------------------------------------------------------ + // + // Protected Properties + // + //------------------------------------------------------ + + /// + /// Gets a value indicating whether isRefreshDeferred there is still an outstanding + /// DeferRefresh in use. If at all possible, derived classes should not call Refresh + /// if IsRefreshDeferred is true. + /// + protected bool IsRefreshDeferred + { + get + { + return _deferLevel > 0; + } + } + + /// + /// Gets a value indicating whether CurrentItem and CurrentPosition are + /// up-to-date with the state and content of the collection. + /// + protected bool IsCurrentInSync + { + get + { + if (IsCurrentInView) + { + return GetItemAt(CurrentPosition) == CurrentItem; + } + else + { + return CurrentItem == null; + } + } + } + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + /// + /// Returns type of the items in the source collection. + /// + /// Type of the items in the source collection. + internal Type GetItemType(bool useRepresentativeItem) + { + Type collectionType = SourceCollection.GetType(); + Type[] interfaces = collectionType.GetInterfaces(); + + // Look for IEnumerable. All generic collections should implement + // this. We loop through the interface list, rather than call + // GetInterface(IEnumerableT), so that we handle an ambiguous match + // (by using the first match) without an exception. + for (int i = 0; i < interfaces.Length; ++i) + { + Type interfaceType = interfaces[i]; + if (interfaceType.Name == IEnumerableT) + { + // found IEnumerable<>, extract T + Type[] typeParameters = interfaceType.GetGenericArguments(); + if (typeParameters.Length == 1) + { + return typeParameters[0]; + } + } + } + + // No generic information found. Use a representative item instead. + if (useRepresentativeItem) + { + // get type of a representative item + object item = GetRepresentativeItem(); + if (item != null) + { + return item.GetType(); + } + } + + return null; + } + + internal object GetRepresentativeItem() + { + if (IsEmpty) + { + return null; + } + + IEnumerator ie = this.GetEnumerator(); + while (ie.MoveNext()) + { + object item = ie.Current; + if (item != null) + { + return item; + } + } + + return null; + } + + internal void RefreshInternal() + { + RefreshOverride(); + + SetFlag(CollectionViewFlags.NeedsRefresh, false); + } + + // helper to validate that we are not in the middle of a DeferRefresh + // and throw if that is the case. + internal void VerifyRefreshNotDeferred() + { + // If the Refresh is being deferred to change filtering or sorting of the + // data by this CollectionView, then CollectionView will not reflect the correct + // state of the underlying data. + if (IsRefreshDeferred) + { + throw CollectionViewsError.CollectionView.NoAccessWhileChangesAreDeferred(); + } + } + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + internal SimpleMonitor CurrentChangedMonitor + { + get + { + return _currentChangedMonitor; + } + } + + internal object SyncRoot + { + get + { + ICollection collection = SourceCollection as ICollection; + if (collection != null && collection.SyncRoot != null) + { + return collection.SyncRoot; + } + else + { + return SourceCollection; + } + } + } + + // Timestamp is used by the PlaceholderAwareEnumerator to determine if a + // collection change has occurred since the enumerator began. (If so, + // MoveNext should throw.) + internal int Timestamp + { + get + { + return _timestamp; + } + } + + //------------------------------------------------------ + // + // Internal Types + // + //------------------------------------------------------ + internal class PlaceholderAwareEnumerator : IEnumerator + { + private enum Position + { + /// + /// Whether the position is before the new item + /// + BeforeNewItem, + + /// + /// Whether the position is on the new item that is being created + /// + OnNewItem, + + /// + /// Whether the position is after the new item + /// + AfterNewItem + } + + public PlaceholderAwareEnumerator(CollectionView collectionView, IEnumerator baseEnumerator, object newItem) + { + _collectionView = collectionView; + _timestamp = collectionView.Timestamp; + _baseEnumerator = baseEnumerator; + _newItem = newItem; + } + + public bool MoveNext() + { + if (_timestamp != _collectionView.Timestamp) + { + throw CollectionViewsError.CollectionView.EnumeratorVersionChanged(); + } + + if (_position == Position.BeforeNewItem) + { + if (_baseEnumerator.MoveNext() && + (_newItem == NoNewItem || _baseEnumerator.Current != _newItem || _baseEnumerator.MoveNext())) + { + // advance base, skipping the new item + } + else if (_newItem != NoNewItem) + { + // if base has reached the end, move to new item + _position = Position.OnNewItem; + } + else + { + return false; + } + + return true; + } + + // in all other cases, simply advance base, skipping the new item + _position = Position.AfterNewItem; + return _baseEnumerator.MoveNext() && + (_newItem == NoNewItem || _baseEnumerator.Current != _newItem || _baseEnumerator.MoveNext()); + } + + public object Current + { + get + { + return (_position == Position.OnNewItem) ? _newItem : _baseEnumerator.Current; + } + } + + public void Reset() + { + _position = Position.BeforeNewItem; + _baseEnumerator.Reset(); + } + + private CollectionView _collectionView; + private IEnumerator _baseEnumerator; + private Position _position; + private object _newItem; + private int _timestamp; + } + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + private bool IsCurrentInView + { + get + { + VerifyRefreshNotDeferred(); + return CurrentPosition >= 0 && CurrentPosition < Count; + } + } + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + // returns true if ANY flag in flags is set. + private bool CheckFlag(CollectionViewFlags flags) + { + return (_flags & flags) != 0; + } + + private void SetFlag(CollectionViewFlags flags, bool value) + { + if (value) + { + _flags = _flags | flags; + } + else + { + _flags = _flags & ~flags; + } + } + + private void EndDefer() + { + _deferLevel--; + + if (_deferLevel == 0 && CheckFlag(CollectionViewFlags.NeedsRefresh)) + { + Refresh(); + } + } + + /// + /// Helper to raise a PropertyChanged event />). + /// + private void OnPropertyChanged(string propertyName) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + //------------------------------------------------------ + // + // Private Types + // + //------------------------------------------------------ + + /// + /// IDisposable deferral object. + /// + private class DeferHelper : IDisposable + { + public DeferHelper(CollectionView collectionView) + { + _collectionView = collectionView; + } + + public void Dispose() + { + if (_collectionView != null) + { + _collectionView.EndDefer(); + _collectionView = null; + } + + GC.SuppressFinalize(this); + } + + private CollectionView _collectionView; + } + + // this class helps prevent reentrant calls + internal class SimpleMonitor : IDisposable + { + public bool Enter() + { + if (_entered) + { + return false; + } + + _entered = true; + return true; + } + + public void Dispose() + { + _entered = false; + GC.SuppressFinalize(this); + } + + public bool Busy + { + get + { + return _entered; + } + } + + private bool _entered; + } + + // Private members and types + [Flags] + private enum CollectionViewFlags + { + ShouldProcessCollectionChanged = 0x4, + IsCurrentBeforeFirst = 0x8, + IsCurrentAfterLast = 0x10, + NeedsRefresh = 0x80, + CachedIsEmpty = 0x200, + } + + // Property names + internal const string CountPropertyName = "Count"; + internal const string IsEmptyPropertyName = "IsEmpty"; + internal const string CulturePropertyName = "Culture"; + internal const string CurrentPositionPropertyName = "CurrentPosition"; + internal const string CurrentItemPropertyName = "CurrentItem"; + internal const string IsCurrentBeforeFirstPropertyName = "IsCurrentBeforeFirst"; + internal const string IsCurrentAfterLastPropertyName = "IsCurrentAfterLast"; + + // since there's nothing in the uncancelable event args that is mutable, + // just create one instance to be used universally. + private static readonly CurrentChangingEventArgs UncancelableCurrentChangingEventArgs = new CurrentChangingEventArgs(false); + private static readonly string IEnumerableT = typeof(IEnumerable<>).Name; + internal static readonly object NoNewItem = new object(); + + // State + private IEnumerable _sourceCollection; + private CollectionViewFlags _flags = CollectionViewFlags.ShouldProcessCollectionChanged | CollectionViewFlags.NeedsRefresh; + private int _timestamp; + private object _currentItem; + private int _currentPosition; + private CultureInfo _culture; + private int _deferLevel; + private SimpleMonitor _currentChangedMonitor = new SimpleMonitor(); + private WeakEventListener _sourceWeakEventListener; +#if FEATURE_ICOLLECTIONVIEW_FILTER + private Predicate _filter; +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionViewsError.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionViewsError.cs new file mode 100644 index 0000000..f906f26 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/CollectionViewsError.cs @@ -0,0 +1,78 @@ +// 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; +using System.Globalization; + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + internal static class CollectionViewsError + { + public static class CollectionView + { + public static InvalidOperationException EnumeratorVersionChanged() + { + return new InvalidOperationException("Collection was modified; enumeration operation cannot execute."); + } + + public static InvalidOperationException MemberNotAllowedDuringAddOrEdit(string paramName) + { + return new InvalidOperationException(Format("'{0}' is not allowed during an AddNew or EditItem transaction.", paramName)); + } + + public static InvalidOperationException NoAccessWhileChangesAreDeferred() + { + return new InvalidOperationException("This value cannot be accessed while changes are deferred."); + } + + public static InvalidOperationException ItemNotAtIndex(string paramName) + { + return new InvalidOperationException(Format("The {0} item is not in the collection.", paramName)); + } + } + + public static class EnumerableCollectionView + { + public static InvalidOperationException RemovedItemNotFound() + { + return new InvalidOperationException("The removed item is not found in the source collection."); + } + } + + public static class ListCollectionView + { + public static InvalidOperationException CollectionChangedOutOfRange() + { + return new InvalidOperationException("The collection change is out of bounds of the original collection."); + } + + public static InvalidOperationException AddedItemNotInCollection() + { + return new InvalidOperationException("The added item is not in the collection."); + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + public static InvalidOperationException CancelEditNotSupported() + { + return new InvalidOperationException("CancelEdit is not supported for the current edit item."); + } + + public static InvalidOperationException MemberNotAllowedDuringTransaction(string paramName1, string paramName2) + { + return new InvalidOperationException(Format("'{0}' is not allowed during a transaction started by '{1}'.", paramName1, paramName2)); + } + + public static InvalidOperationException MemberNotAllowedForView(string paramName) + { + return new InvalidOperationException(Format("'{0}' is not allowed for this view.", paramName)); + } +#endif + } + + private static string Format(string formatString, params object[] args) + { + return string.Format(CultureInfo.CurrentCulture, formatString, args); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/EnumerableCollectionView.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/EnumerableCollectionView.cs new file mode 100644 index 0000000..8a1d9d0 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/EnumerableCollectionView.cs @@ -0,0 +1,697 @@ +// 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; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + /// + /// A collection view implementation that supports an IEnumerable source. + /// + internal class EnumerableCollectionView : CollectionView + { + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + // Set up a ListCollectionView over the snapshot. + // We will delegate all CollectionView functionality to this view. + internal EnumerableCollectionView(IEnumerable source) + : base(source) + { + _snapshot = new ObservableCollection(); + + LoadSnapshotCore(source); + + if (_snapshot.Count > 0) + { + SetCurrent(_snapshot[0], 0, 1); + } + else + { + SetCurrent(null, -1, 0); + } + + // If the source doesn't raise collection change events, try to detect changes by polling the enumerator. + _pollForChanges = !(source is INotifyCollectionChanged); + + _view = new ListCollectionView(_snapshot); + + INotifyCollectionChanged incc = _view as INotifyCollectionChanged; + incc.CollectionChanged += new NotifyCollectionChangedEventHandler(EnumerableCollectionView_OnViewChanged); + + INotifyPropertyChanged ipc = _view as INotifyPropertyChanged; + ipc.PropertyChanged += new PropertyChangedEventHandler(EnumerableCollectionView_OnPropertyChanged); + + _view.CurrentChanging += new CurrentChangingEventHandler(EnumerableCollectionView_OnCurrentChanging); + _view.CurrentChanged += new EventHandler(EnumerableCollectionView_OnCurrentChanged); + } + + //------------------------------------------------------ + // + // Interfaces + // + //------------------------------------------------------ + + /// + /// Gets or sets culture to use during sorting. + /// + public override CultureInfo Culture + { + get + { + return _view.Culture; + } + + set + { + _view.Culture = value; + } + } + + /// + /// Return true if the item belongs to this view. No assumptions are + /// made about the item. This method will behave similarly to IList.Contains(). + /// If the caller knows that the item belongs to the + /// underlying collection, it is more efficient to call PassesFilter. + /// + /// True if the item belongs to this view. + public override bool Contains(object item) + { + EnsureSnapshot(); + return _view.Contains(item); + } + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Set/get a filter callback to filter out items in collection. + /// This property will always accept a filter, but the collection view for the + /// underlying InnerList or ItemsSource may not actually support filtering. + /// Please check + /// + /// + /// Collections assigned to ItemsSource may not support filtering and could throw a NotSupportedException. + /// Use property to test if sorting is supported before adding + /// to SortDescriptions. + /// + public override Predicate Filter + { + get + { + return _view.Filter; + } + + set + { + _view.Filter = value; + } + } + + /// + /// Test if this ICollectionView supports filtering before assigning + /// a filter callback to . + /// + public override bool CanFilter + { + get + { + return _view.CanFilter; + } + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Set/get Sort criteria to sort items in collection. + /// + /// + ///

+ /// Clear a sort criteria by assigning SortDescription.Empty to this property. + /// One or more sort criteria in form of + /// can be used, each specifying a property and direction to sort by. + ///

+ ///
+ /// + /// Simpler implementations do not support sorting and will throw a NotSupportedException. + /// Use property to test if sorting is supported before adding + /// to SortDescriptions. + /// + public override SortDescriptionCollection SortDescriptions + { + get + { + return _view.SortDescriptions; + } + } + + /// + /// Test if this ICollectionView supports sorting before adding + /// to . + /// + public override bool CanSort + { + get + { + return _view.CanSort; + } + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_GROUP + /// + /// Returns true if this view really supports grouping. + /// When this returns false, the rest of the interface is ignored. + /// + public override bool CanGroup + { + get + { + return _view.CanGroup; + } + } + + /// + /// The description of grouping, indexed by level. + /// + public override ObservableCollection GroupDescriptions + { + get + { + return _view.GroupDescriptions; + } + } + + /// + /// The top-level groups, constructed according to the descriptions + /// given in GroupDescriptions. + /// + public override ReadOnlyObservableCollection Groups + { + get + { + return _view.Groups; + } + } +#endif + + /// + /// Enter a Defer Cycle. + /// Defer cycles are used to coalesce changes to the ICollectionView. + /// + /// An IDisposable deferral object. + public override IDisposable DeferRefresh() + { + return _view.DeferRefresh(); + } + + /// + /// Gets the current item. + /// + public override object CurrentItem + { + get + { + return _view.CurrentItem; + } + } + + /// + /// Gets the ordinal position of the within the (optionally + /// sorted and filtered) view. + /// + public override int CurrentPosition + { + get + { + return _view.CurrentPosition; + } + } + + /// + /// Gets a value indicating whether the currency is beyond the end (End-Of-File). + /// + public override bool IsCurrentAfterLast + { + get + { + return _view.IsCurrentAfterLast; + } + } + + /// + /// Gets a value indicating whether the currency is before the beginning (Beginning-Of-File). + /// + public override bool IsCurrentBeforeFirst + { + get + { + return _view.IsCurrentBeforeFirst; + } + } + + /// + /// Move to the first item. + /// + /// True if points to an item within the view. + public override bool MoveCurrentToFirst() + { + return _view.MoveCurrentToFirst(); + } + + /// + /// Move to the previous item. + /// + /// True if points to an item within the view. + public override bool MoveCurrentToPrevious() + { + return _view.MoveCurrentToPrevious(); + } + + /// + /// Move to the next item. + /// + /// True if points to an item within the view. + public override bool MoveCurrentToNext() + { + return _view.MoveCurrentToNext(); + } + + /// + /// Move to the last item. + /// + /// True if points to an item within the view. + public override bool MoveCurrentToLast() + { + return _view.MoveCurrentToLast(); + } + + /// + /// Move to the given item. + /// If the item is not found, move to BeforeFirst. + /// + /// Move CurrentItem to this item. + /// True if points to an item within the view. + public override bool MoveCurrentTo(object item) + { + return _view.MoveCurrentTo(item); + } + + /// + /// Move to the item at the given index. + /// + /// Move CurrentItem to this index + /// True if points to an item within the view. + public override bool MoveCurrentToPosition(int position) + { + // If the index is out of range here, I'll let the + // _view be the one to make that determination. + return _view.MoveCurrentToPosition(position); + } + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + /// + /// Gets the number of records (or -1, meaning "don't know"). + /// A virtualizing view should return the best estimate it can + /// without de-virtualizing all the data. A non-virtualizing view + /// should return the exact count of its (filtered) data. + /// + public override int Count + { + get + { + EnsureSnapshot(); + return _view.Count; + } + } + + public override bool IsEmpty + { + get + { + EnsureSnapshot(); + return (_view != null) ? _view.IsEmpty : true; + } + } + + /// + /// Gets a value indicating whether this view needs to be refreshed. + /// + public override bool NeedsRefresh + { + get + { + return _view.NeedsRefresh; + } + } + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + /// + /// Return the index where the given item appears, or -1 if doesn't appear. + /// + /// data item + /// The index where the given item belongs, or -1 if this index is unknown. + public override int IndexOf(object item) + { + EnsureSnapshot(); + return _view.IndexOf(item); + } + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Return true if the item belongs to this view. The item is assumed to belong to the + /// underlying DataCollection; this method merely takes filters into account. + /// It is commonly used during collection-changed notifications to determine if the added/removed + /// item requires processing. + /// Returns true if no filter is set on collection view. + /// + /// True if the item belongs to this view. + public override bool PassesFilter(object item) + { + if (_view.CanFilter && _view.Filter != null) + { + return _view.Filter(item); + } + + return true; + } +#endif + + /// + /// Retrieve item at the given zero-based index in this CollectionView. + /// + /// + /// Thrown if index is out of range + /// + /// Item at the given zero-based index in this CollectionView. + public override object GetItemAt(int index) + { + EnsureSnapshot(); + return _view.GetItemAt(index); + } + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + /// Implementation of IEnumerable.GetEnumerator(). + /// This provides a way to enumerate the members of the collection + /// without changing the currency. + /// + /// IEnumerator object that enumerates the items in this view. + protected override IEnumerator GetEnumerator() + { + EnsureSnapshot(); + return (_view as IEnumerable).GetEnumerator(); + } + + /// + /// Re-create the view over the associated IList + /// + /// + /// Any sorting and filtering will take effect during Refresh. + /// + protected override void RefreshOverride() + { + LoadSnapshot(SourceCollection); + } + + /// + /// Processes a single collection change on the UI thread. + /// + /// + /// The NotifyCollectionChangedEventArgs to be processed. + /// + protected override void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) + { + // Apply the change to the snapshot + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + if (args.NewStartingIndex < 0 || _snapshot.Count <= args.NewStartingIndex) + { // Append + for (int i = 0; i < args.NewItems.Count; ++i) + { + _snapshot.Add(args.NewItems[i]); + } + } + else + { // Insert + for (int i = args.NewItems.Count - 1; i >= 0; --i) + { + _snapshot.Insert(args.NewStartingIndex, args.NewItems[i]); + } + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (args.OldStartingIndex < 0) + { + throw CollectionViewsError.EnumerableCollectionView.RemovedItemNotFound(); + } + + for (int i = args.OldItems.Count - 1, index = args.OldStartingIndex + i; i >= 0; --i, --index) + { + if (!object.Equals(args.OldItems[i], _snapshot[index])) + { + throw CollectionViewsError.CollectionView.ItemNotAtIndex("removed"); + } + + _snapshot.RemoveAt(index); + } + + break; + + case NotifyCollectionChangedAction.Replace: + for (int i = args.NewItems.Count - 1, index = args.NewStartingIndex + i; i >= 0; --i, --index) + { + if (!object.Equals(args.OldItems[i], _snapshot[index])) + { + throw CollectionViewsError.CollectionView.ItemNotAtIndex("replaced"); + } + + _snapshot[index] = args.NewItems[i]; + } + + break; + + case NotifyCollectionChangedAction.Reset: + LoadSnapshot(SourceCollection); + break; + } + } + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + // Load a snapshot of the contents of the IEnumerable into the ObservableCollection. + private void LoadSnapshot(IEnumerable source) + { + // Force currency off the collection (gives user a chance to save dirty information). + OnCurrentChanging(); + + // Remember the values of the scalar properties, so that we can restore + // them and raise events after reloading the data + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + + // Reload the data + LoadSnapshotCore(source); + + // Tell listeners everything has changed + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + OnCurrentChanged(); + + if (IsCurrentAfterLast != oldIsCurrentAfterLast) + { + OnPropertyChanged(new PropertyChangedEventArgs(IsCurrentAfterLastPropertyName)); + } + + if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) + { + OnPropertyChanged(new PropertyChangedEventArgs(IsCurrentBeforeFirstPropertyName)); + } + + if (oldCurrentPosition != CurrentPosition) + { + OnPropertyChanged(new PropertyChangedEventArgs(CurrentPositionPropertyName)); + } + + if (oldCurrentItem != CurrentItem) + { + OnPropertyChanged(new PropertyChangedEventArgs(CurrentItemPropertyName)); + } + } + + private void LoadSnapshotCore(IEnumerable source) + { + _trackingEnumerator = source.GetEnumerator(); + + using (IgnoreViewEvents()) + { + _snapshot.Clear(); + + while (_trackingEnumerator.MoveNext()) + { + _snapshot.Add(_trackingEnumerator.Current); + } + } + } + + // If the IEnumerable has changed, bring the snapshot up to date. + // (This isn't necessary if the IEnumerable is also INotifyCollectionChanged + // because we keep the snapshot in sync incrementally.) + private void EnsureSnapshot() + { + if (_pollForChanges) + { + try + { + _trackingEnumerator.MoveNext(); + } + catch (InvalidOperationException) + { + // This "feature" is necessarily incomplete (we cannot detect + // the changes when they happen, only as a side-effect of some + // later operation), and inconsistent (none of the other + // collection views does this). Changing a collection without + // raising a notification is not supported. + + // Collection was changed - start over with a new enumerator + LoadSnapshotCore(SourceCollection); + } + } + } + + private IDisposable IgnoreViewEvents() + { + return new IgnoreViewEventsHelper(this); + } + + private void BeginIgnoreEvents() + { + _ignoreEventsLevel++; + } + + private void EndIgnoreEvents() + { + _ignoreEventsLevel--; + } + + // forward events from the internal view to our own listeners + private void EnumerableCollectionView_OnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (_ignoreEventsLevel != 0) + { + return; + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + // Also ignore ListCollectionView's property change notifications for + // IEditableCollectionView's properties + switch (args.PropertyName) + { + case ListCollectionView.CanAddNewPropertyName: + case ListCollectionView.CanCancelEditPropertyName: + case ListCollectionView.CanRemovePropertyName: + case ListCollectionView.CurrentAddItemPropertyName: + case ListCollectionView.CurrentEditItemPropertyName: + case ListCollectionView.IsAddingNewPropertyName: + case ListCollectionView.IsEditingItemPropertyName: + return; + } +#endif + OnPropertyChanged(args); + } + + private void EnumerableCollectionView_OnViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_ignoreEventsLevel != 0) + { + return; + } + + OnCollectionChanged(args); + } + + private void EnumerableCollectionView_OnCurrentChanging(object sender, CurrentChangingEventArgs args) + { + if (_ignoreEventsLevel != 0) + { + return; + } + + OnCurrentChanging(args); + } + + private void EnumerableCollectionView_OnCurrentChanged(object sender, object args) + { + if (_ignoreEventsLevel != 0) + { + return; + } + + OnCurrentChanged(); + } + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + private ListCollectionView _view; + private ObservableCollection _snapshot; + private IEnumerator _trackingEnumerator; + private int _ignoreEventsLevel; + private bool _pollForChanges; + + private class IgnoreViewEventsHelper : IDisposable + { + public IgnoreViewEventsHelper(EnumerableCollectionView parent) + { + _parent = parent; + _parent.BeginIgnoreEvents(); + } + + public void Dispose() + { + if (_parent != null) + { + _parent.EndIgnoreEvents(); + _parent = null; + } + + GC.SuppressFinalize(this); + } + + private EnumerableCollectionView _parent; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/ListCollectionView.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/ListCollectionView.cs new file mode 100644 index 0000000..1c26430 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/CollectionViews/ListCollectionView.cs @@ -0,0 +1,2518 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +#if FEATURE_IEDITABLECOLLECTIONVIEW +using System.Reflection; // ConstructorInfo +#endif + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + /// + /// A collection view implementation that supports an IList source. + /// + internal class ListCollectionView : CollectionView, IComparer +#if FEATURE_IEDITABLECOLLECTIONVIEW + , IEditableCollectionView +#endif + { + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + /// + /// Initializes a new instance of the class. + /// + /// Underlying IList + public ListCollectionView(IList list) + : base(list) + { + _internalList = list; + +#if FEATURE_IEDITABLECOLLECTIONVIEW + RefreshCanAddNew(); + RefreshCanRemove(); + RefreshCanCancelEdit(); +#endif + if (InternalList.Count == 0) + { + // don't call virtual IsEmpty in ctor + SetCurrent(null, -1, 0); + } + else + { + SetCurrent(InternalList[0], 0, 1); + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + _group = new CollectionViewGroupRoot(this); + _group.GroupDescriptionChanged += new EventHandler(OnGroupDescriptionChanged); + ((INotifyCollectionChanged)_group).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupChanged); + ((INotifyCollectionChanged)_group.GroupDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupByChanged); +#endif + } + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + /// + /// Re-create the view over the associated IList + /// + /// + /// Any sorting and filtering will take effect during Refresh. + /// + protected override void RefreshOverride() + { + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = IsEmpty ? -1 : CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // force currency off the collection (gives user a chance to save dirty information) + OnCurrentChanging(); + + IList list = SourceCollection as IList; +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + PrepareSortAndFilter(); + + // if there's no sort/filter, just use the collection's array + if (!UsesLocalArray) + { +#endif +#pragma warning disable SA1137 // Elements should have the same indentation + _internalList = list; +#pragma warning restore SA1137 // Elements should have the same indentation +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + } + else + { + _internalList = PrepareLocalArray(list); + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_GROUP + PrepareGroups(); +#endif + + if (oldIsCurrentBeforeFirst || IsEmpty) + { + SetCurrent(null, -1); + } + else if (oldIsCurrentAfterLast) + { + SetCurrent(null, InternalCount); + } + else + { + // set currency back to old current item + // oldCurrentItem may be null + + // if there are duplicates, use the position of the first matching item + int newPosition = InternalIndexOf(oldCurrentItem); + + if (newPosition < 0) + { + // oldCurrentItem not found: move to first item + SetCurrent(InternalItemAt(0), 0); + } + else + { + SetCurrent(oldCurrentItem, newPosition); + } + } + + // tell listeners everything has changed + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + RaiseCurrencyChanges( + true /*raiseCurrentChanged*/, + CurrentItem != oldCurrentItem /*raiseCurrentItem*/, + CurrentPosition != oldCurrentPosition /*raiseCurrentPosition*/, + IsCurrentBeforeFirst != oldIsCurrentBeforeFirst /*raiseIsCurrentBeforeFirst*/, + IsCurrentAfterLast != oldIsCurrentAfterLast /*raiseIsCurrentAfterLast*/); + } + + /// + /// Return true if the item belongs to this view. No assumptions are + /// made about the item. This method will behave similarly to IList.Contains() + /// and will do an exhaustive search through all items in this view. + /// If the caller knows that the item belongs to the + /// underlying collection, it is more efficient to call PassesFilter. + /// + /// True if collection contains item. + public override bool Contains(object item) + { + VerifyRefreshNotDeferred(); + + return InternalContains(item); + } + + /// + /// Move CurrentItem to the item at the given index. + /// + /// Move CurrentItem to this index + /// true if CurrentItem points to an item within the view. + public override bool MoveCurrentToPosition(int position) + { + VerifyRefreshNotDeferred(); + + if (position < -1 || position > InternalCount) + { + throw new ArgumentOutOfRangeException("position"); + } + + if ((position != CurrentPosition || !IsCurrentInSync) && OKToChangeCurrent()) + { + object proposedCurrentItem = (position >= 0 && position < InternalCount) ? InternalItemAt(position) : null; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + SetCurrent(proposedCurrentItem, position); + + // notify that the properties have changed. + RaiseCurrencyChanges( + true /*raiseCurrentChanged*/, + true /*raiseCurrentItem*/, + true /*raiseCurrentPosition*/, + IsCurrentBeforeFirst != oldIsCurrentBeforeFirst /*raiseIsCurrentBeforeFirst*/, + IsCurrentAfterLast != oldIsCurrentAfterLast /*raiseIsCurrentAfterLast*/); + } + + return IsCurrentInView; + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + /// + /// Gets a value indicating whether this view really supports grouping. + /// When this returns false, the rest of the interface is ignored. + /// + public override bool CanGroup + { + get + { + return true; + } + } + + /// + /// Gets the description of grouping, indexed by level. + /// + public override ObservableCollection GroupDescriptions + { + get + { + return _group.GroupDescriptions; + } + } + + /// + /// Gets the top-level groups, constructed according to the descriptions + /// given in GroupDescriptions. + /// + public override ReadOnlyObservableCollection Groups + { + get + { + return IsGrouping ? _group.Items : null; + } + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Return true if the item belongs to this view. The item is assumed to belong to the + /// underlying DataCollection; this method merely takes filters into account. + /// It is commonly used during collection-changed notifications to determine if the added/removed + /// item requires processing. + /// Returns true if no filter is set on collection view. + /// + /// True if the item belongs to this view. + public override bool PassesFilter(object item) + { + return ActiveFilter == null || ActiveFilter(item); + } +#endif + + /// + /// Return the index where the given item belongs, or -1 if this index is unknown. + /// + /// + /// If this method returns an index other than -1, it must always be true that + /// view[index-1] < item <= view[index], where the comparisons are done via + /// the view's IComparer.Compare method (if any). + /// (This method is used by a listener's (e.g. System.Windows.Controls.ItemsControl) + /// CollectionChanged event handler to speed up its reaction to insertion and deletion of items. + /// If IndexOf is not implemented, a listener does a binary search using IComparer.Compare.) + /// + /// data item + /// The index where the given item belongs, or -1 if this index is unknown. + public override int IndexOf(object item) + { + VerifyRefreshNotDeferred(); + + return InternalIndexOf(item); + } + + /// + /// Retrieve item at the given zero-based index in this CollectionView. + /// + /// + ///

The index is evaluated with any SortDescriptions or Filter being set on this CollectionView.

+ ///
+ /// + /// Thrown if index is out of range + /// + /// Item at the given zero-based index in this CollectionView. + public override object GetItemAt(int index) + { + VerifyRefreshNotDeferred(); + + return InternalItemAt(index); + } + + /// + /// Return -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively) + /// + /// first object + /// second object + /// + /// Compares items by their resp. index in the IList. + /// + /// -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively). + int IComparer.Compare(object o1, object o2) + { + return Compare(o1, o2); + } + + /// + /// Return -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively) + /// + /// first object + /// second object + /// + /// Compares items by their resp. index in the IList. + /// + /// Return -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively). + protected virtual int Compare(object o1, object o2) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (!IsGrouping) + { +#endif +#if FEATURE_ICOLLECTIONVIEW_SORT + if (ActiveComparer != null) + { + return ActiveComparer.Compare(o1, o2); + } +#endif + int i1 = InternalList.IndexOf(o1); + int i2 = InternalList.IndexOf(o2); + return i1 - i2; +#if FEATURE_ICOLLECTIONVIEW_GROUP + } + else + { + int i1 = InternalIndexOf(o1); + int i2 = InternalIndexOf(o2); + return i1 - i2; + } +#endif + } + + /// + /// Implementation of IEnumerable.GetEnumerator(). + /// This provides a way to enumerate the members of the collection + /// without changing the currency. + /// + /// IEnumerator + protected override IEnumerator GetEnumerator() + { + VerifyRefreshNotDeferred(); + + return InternalGetEnumerator(); + } + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Gets a collection of Sort criteria to sort items in this view over the SourceCollection. + /// + /// + ///

+ /// One or more sort criteria in form of SortDescription + /// can be added, each specifying a property and direction to sort by. + ///

+ ///
+ public override SortDescriptionCollection SortDescriptions + { + get + { + if (_sort == null) + { + SetSortDescriptions(new SortDescriptionCollection()); + } + + return _sort; + } + } + + /// + /// Gets a value indicating whether this ICollectionView supports sorting. + /// + /// + /// ListCollectionView does implement an IComparer based sorting. + /// + public override bool CanSort + { + get + { + return true; + } + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Gets a value indicating whether ICollectionView supports filtering. + /// + public override bool CanFilter + { + get + { + return true; + } + } +#endif + +#if FEATURE_IEDITABLECOLLECTIONVIEW + /// + /// Gets or sets the callback used by the implementation of the ICollectionView to determine if an + /// item is suitable for inclusion in the view. + /// + /// + /// Simpler implementations do not support filtering and will throw a NotSupportedException. + /// Use property to test if filtering is supported before + /// assigning a non-null value. + /// + public override Predicate Filter + { + get + { + return base.Filter; + } + + set + { + if (IsAddingNew || IsEditingItem) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("Filter"); + } + base.Filter = value; + } + } +#endif + + /// + /// Gets the estimated number of records (or -1, meaning "don't know"). + /// + public override int Count + { + get + { + VerifyRefreshNotDeferred(); + + return InternalCount; + } + } + + /// + /// Gets a value indicating whether the resulting (filtered) view is empty. + /// + public override bool IsEmpty + { + get + { + return InternalCount == 0; + } + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + /// + /// Gets or sets a value that indicates whether to include a placeholder for a new item, and if so, + /// where to put it. + /// + public NewItemPlaceholderPosition NewItemPlaceholderPosition + { + get + { + return NewItemPlaceholderPosition.None; + } + + set + { + if ((NewItemPlaceholderPosition)value != NewItemPlaceholderPosition.None) + { + throw new ArgumentException("value"); + } + } + } + + /// + /// Gets a value indicating whether the view supports AddNew. + /// + public bool CanAddNew + { + get + { + return _canAddNew; + } + + private set + { + if (_canAddNew != value) + { + _canAddNew = value; + OnPropertyChanged(CanAddNewPropertyName); + } + } + } + + private void RefreshCanAddNew() + { + this.CanAddNew = !IsEditingItem && SourceList != null && !SourceList.IsFixedSize && CanConstructItem; + } + + private bool CanConstructItem + { + get + { + if (!_isItemConstructorValid) + { + EnsureItemConstructor(); + } + + return _itemConstructor != null; + } + } + + private void EnsureItemConstructor() + { + if (!_isItemConstructorValid) + { + Type itemType = GetItemType(true); + if (itemType != null) + { + _itemConstructor = itemType.GetConstructor(Type.EmptyTypes); + _isItemConstructorValid = true; + } + } + } + + /// + /// Add a new item to the underlying collection. Returns the new item. + /// After calling AddNew and changing the new item as desired, either + /// or should be + /// called to complete the transaction. + /// + /// The new item. + public object AddNew() + { + VerifyRefreshNotDeferred(); + + if (IsEditingItem) + { + CommitEdit(); // implicitly close a previous EditItem + } + + CommitNew(); // implicitly close a previous AddNew + + if (!CanAddNew) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedForView("AddNew"); + } + + object newItem = _itemConstructor.Invoke(null); + + _newItemIndex = -2; // this is a signal that the next Add event comes from AddNew + int index = SourceList.Add(newItem); + + // if the source doesn't raise collection change events, fake one + if (!(SourceList is INotifyCollectionChanged)) + { + // the index returned by IList.Add isn't always reliable + if (!object.Equals(newItem, SourceList[index])) + { + index = SourceList.IndexOf(newItem); + } + + BeginAddNew(newItem, index); + } + + Debug.Assert(_newItemIndex != -2 && object.Equals(newItem, _newItem), "AddNew did not raise expected events"); + + MoveCurrentTo(newItem); + + ISupportInitialize isi = newItem as ISupportInitialize; + if (isi != null) + { + isi.BeginInit(); + } + + IEditableObject ieo = newItem as IEditableObject; + if (ieo != null) + { + ieo.BeginEdit(); + } + + return newItem; + } + + // Calling IList.Add() will raise an ItemAdded event. We handle this specially + // to adjust the position of the new item in the view (it should be adjacent + // to the placeholder), and cache the new item for use by the other APIs + // related to AddNew. This method is called from ProcessCollectionChanged. + private void BeginAddNew(object newItem, int index) + { + Debug.Assert(_newItemIndex == -2 && _newItem == NoNewItem, "unexpected call to BeginAddNew"); + + // remember the new item and its position in the underlying list + SetNewItem(newItem); + _newItemIndex = index; + + // adjust the position of the new item + int position = UsesLocalArray ? InternalCount - 1 : _newItemIndex; + + // raise events as if the new item appeared in the adjusted position + ProcessCollectionChangedWithAdjustedIndex( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + newItem, + position), + -1, + position); + } + + /// + /// Complete the transaction started by . The new + /// item remains in the collection, and the view's sort, filter, and grouping + /// specifications (if any) are applied to the new item. + /// + public void CommitNew() + { + if (IsEditingItem) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedDuringTransaction("CommitNew", "EditItem"); + } + + VerifyRefreshNotDeferred(); + + if (_newItem == NoNewItem) + { + return; + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + // grouping works differently + if (IsGrouping) + { + CommitNewForGrouping(); + return; + } +#endif + + // Remember its current position (have to do this before calling EndNew, + // because InternalCount depends on "adding-new" mode). + int fromIndex = UsesLocalArray ? InternalCount - 1 : _newItemIndex; + + // End the AddNew transaction + object newItem = EndAddNew(false); + + // Tell the view clients what happened to the new item + int toIndex = AdjustBefore(NotifyCollectionChangedAction.Add, newItem, _newItemIndex); + + if (toIndex < 0) + { + // item is effectively removed (due to filter), raise a Remove event + ProcessCollectionChangedWithAdjustedIndex( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + newItem, + fromIndex), + fromIndex, + -1); + } + else if (fromIndex == toIndex) + { + // item isn't moving, so no events are needed. But the item does need + // to be added to the local array. + if (UsesLocalArray) + { + InternalList.Insert(toIndex, newItem); + } + } + else + { + // item is moving + ProcessCollectionChangedWithAdjustedIndex(newItem, fromIndex, toIndex); + } + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + private void CommitNewForGrouping() + { + // for grouping we cannot pretend that the new item moves to a different position, + // since it may actually appear in several new positions (belonging to several groups). + // Instead, we remove the item from its temporary position, then add it to the groups + // as if it had just been added to the underlying collection. + int index = _group.Items.Count - 1; + + // End the AddNew transaction + int newItemIndex = _newItemIndex; + object newItem = EndAddNew(false); + + try + { + _newGroupedItem = newItem; + + // remove item from its temporary position + _group.RemoveSpecialItem(index, newItem, false /*loading*/); + + // now pretend it just got added to the collection. This will add it + // to the internal list with sort/filter, and to the groups + ProcessCollectionChanged( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + newItem, + newItemIndex)); + } + finally + { + _newGroupedItem = null; + } + + // Check if currency was already set to a particular item + if (CurrentPosition == -1) + { + // Attempt to move to the new item. CurrentPosition will remain -1 + // if the new item cannot be found or it does not pass the filter. + MoveCurrentTo(newItem); + } + } +#endif + + /// + /// Complete the transaction started by . The new + /// item is removed from the collection. + /// + public void CancelNew() + { + if (IsEditingItem) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedDuringTransaction("CancelNew", "EditItem"); + } + + VerifyRefreshNotDeferred(); + + if (_newItem == NoNewItem) + { + return; + } + + // remove the new item from the underlying collection. Normally the + // collection will raise a Remove event, which we'll handle by calling + // EndNew to leave AddNew mode. + SourceList.RemoveAt(_newItemIndex); + + // if the collection doesn't raise events, do the work explicitly on its behalf + if (_newItem != NoNewItem) + { + int index = AdjustBefore(NotifyCollectionChangedAction.Remove, _newItem, _newItemIndex); + object newItem = EndAddNew(true); + + ProcessCollectionChangedWithAdjustedIndex( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + newItem, + index), + index, + -1); + } + } + + // Common functionality used by CommitNew, CancelNew, and when the + // new item is removed by Remove or Refresh. + private object EndAddNew(bool cancel) + { + object newItem = _newItem; + + SetNewItem(NoNewItem); // leave "adding-new" mode + + IEditableObject ieo = newItem as IEditableObject; + if (ieo != null) + { + if (cancel) + { + ieo.CancelEdit(); + } + else + { + ieo.EndEdit(); + } + } + + ISupportInitialize isi = newItem as ISupportInitialize; + if (isi != null) + { + isi.EndInit(); + } + + return newItem; + } + + /// + /// Gets a value indicating whether an AddNew transaction is in progress. + /// + public bool IsAddingNew + { + get + { + return _newItem != NoNewItem; + } + } + + /// + /// Gets the new item when an AddNew transaction is in progress. Otherwise it returns null. + /// + public object CurrentAddItem + { + get + { + return IsAddingNew ? _newItem : null; + } + } + + private void SetNewItem(object item) + { + if (!object.Equals(item, _newItem)) + { + Debug.Assert(item == NoNewItem || this._newItem == NoNewItem, "Old and new _newItem values are unexpectedly different from NoNewItem"); + _newItem = item; + + OnPropertyChanged(CurrentAddItemPropertyName); + OnPropertyChanged(IsAddingNewPropertyName); + RefreshCanRemove(); + } + } + + /// + /// Gets a value indicating whether the view supports Remove and RemoveAt. + /// + public bool CanRemove + { + get + { + return _canRemove; + } + + private set + { + if (_canRemove != value) + { + _canRemove = value; + OnPropertyChanged(CanRemovePropertyName); + } + } + } + + private void RefreshCanRemove() + { + this.CanRemove = !IsEditingItem && !IsAddingNew && !SourceList.IsFixedSize; + } + + /// + /// Remove the item at the given index from the underlying collection. + /// The index is interpreted with respect to the view (not with respect to + /// the underlying collection). + /// + public void RemoveAt(int index) + { + if (IsEditingItem || IsAddingNew) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("RemoveAt"); + } + else if (!this.CanRemove) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedForView("RemoveAt"); + } + + VerifyRefreshNotDeferred(); + + // convert the index from "view-relative" to "list-relative" + object item = GetItemAt(index); + + int listIndex = index; + bool raiseEvent = !(SourceList is INotifyCollectionChanged); + + // remove the item from the list + if (UsesLocalArray || IsGrouping) + { + if (raiseEvent) + { + listIndex = SourceList.IndexOf(item); + SourceList.RemoveAt(listIndex); + } + else + { + SourceList.Remove(item); + } + } + else + { + SourceList.RemoveAt(listIndex); + } + + // if the list doesn't raise CollectionChanged events, fake one + if (raiseEvent) + { + ProcessCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + item, + listIndex)); + } + } + + /// + /// Remove the given item from the underlying collection. + /// + public void Remove(object item) + { + int index = InternalIndexOf(item); + if (index >= 0) + { + RemoveAt(index); + } + } + + /// + /// Begins an editing transaction on the given item. The transaction is + /// completed by calling either or + /// . Any changes made to the item during + /// the transaction are considered "pending", provided that the view supports + /// the notion of "pending changes" for the given item. + /// + public void EditItem(object item) + { + VerifyRefreshNotDeferred(); + + if (IsAddingNew) + { + if (object.Equals(item, _newItem)) + { + return; // EditItem(newItem) is a no-op + } + + CommitNew(); // implicitly close a previous AddNew + } + + CommitEdit(); // implicitly close a previous EditItem transaction + + SetEditItem(item); + + IEditableObject ieo = item as IEditableObject; + if (ieo != null) + { + ieo.BeginEdit(); + } + } + + /// + /// Complete the transaction started by . + /// The pending changes (if any) to the item are committed. + /// + public void CommitEdit() + { + if (IsAddingNew) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedDuringTransaction("CommitEdit", "AddNew"); + } + + VerifyRefreshNotDeferred(); + + if (_editItem == null) + { + return; + } + + object editItem = _editItem; + SetEditItem(null); + + IEditableObject ieo = editItem as IEditableObject; + if (ieo != null) + { + ieo.EndEdit(); + } + + int fromIndex = -1; + bool wasInView = false, isInView = false; + if (IsGrouping || UsesLocalArray) + { + // see if the item is entering or leaving the view + fromIndex = InternalIndexOf(editItem); + wasInView = fromIndex >= 0; + isInView = wasInView ? PassesFilter(editItem) + : SourceList.Contains(editItem) && PassesFilter(editItem); + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + // editing may change the item's group names (and we can't tell whether + // it really did). The best we can do is remove the item and re-insert + // it. + if (IsGrouping) + { + // Check whether to restore currency to the item being edited + object restoreCurrencyTo = (editItem == CurrentItem) ? editItem : null; + + if (wasInView) + { + RemoveItemFromGroups(editItem); + } + + // Cache currency values so the appropriate PropertyChanged events can be raised later + // Values are cached after calling RemoveItemFromGroups since that method may change + // currency and raise events itself. + object oldCurrentItem = CurrentItem; + int oldCurrentPosition = CurrentPosition; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + if (isInView) + { + AddItemToGroups(editItem); + } + + // Check if currency was already set to a particular item, + // if the edited item was the current item and may need to be restored + if (CurrentPosition == -1 && restoreCurrencyTo != null) + { + // Check if edited item ended up in the view and if it's OK to change currency + int newPosition = InternalIndexOf(restoreCurrencyTo); + if (newPosition >= 0 && PassesFilter(restoreCurrencyTo) && OKToChangeCurrent()) + { + // Restore the original currency + SetCurrent(restoreCurrencyTo, newPosition); + } + } + + RaiseCurrencyChanges( + oldCurrentItem != CurrentItem /*raiseCurrentChanged*/, + CurrentItem != oldCurrentItem /*raiseCurrentItem*/, + CurrentPosition != oldCurrentPosition /*raiseCurrentPosition*/, + IsCurrentBeforeFirst != oldIsCurrentBeforeFirst /*raiseIsCurrentBeforeFirst*/, + IsCurrentAfterLast != oldIsCurrentAfterLast /*raiseIsCurrentAfterLast*/); + return; + } +#endif + + // the edit may cause the item to move. If so, report it. + if (UsesLocalArray) + { + List list = InternalList as List; + int toIndex = -1; + + if (wasInView) + { + if (!isInView) + { + // the item has been effectively removed + ProcessCollectionChangedWithAdjustedIndex( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, + editItem, + fromIndex), + fromIndex, + -1); + } + else if (ActiveComparer != null) + { + // the item may have moved within the view + int localIndex = fromIndex; + if (localIndex > 0 && ActiveComparer.Compare(list[localIndex - 1], editItem) > 0) + { + // the item has moved toward the front of the list + toIndex = list.BinarySearch(0, localIndex, editItem, ActiveComparer); + if (toIndex < 0) + { + toIndex = ~toIndex; + } + } + else if (localIndex < list.Count - 1 && ActiveComparer.Compare(editItem, list[localIndex + 1]) > 0) + { + // the item has moved toward the back of the list + toIndex = list.BinarySearch(localIndex + 1, list.Count - localIndex - 1, editItem, ActiveComparer); + if (toIndex < 0) + { + toIndex = ~toIndex; + } + + --toIndex; // because the item is leaving its old position + } + + if (toIndex >= 0) + { + // the item has effectively moved + ProcessCollectionChangedWithAdjustedIndex(editItem, fromIndex, toIndex); + } + } + } + else if (isInView) + { + // the item has effectively been added + toIndex = AdjustBefore(NotifyCollectionChangedAction.Add, editItem, SourceList.IndexOf(editItem)); + ProcessCollectionChangedWithAdjustedIndex( + new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, + editItem, + toIndex), + -1, + toIndex); + } + } + } + + /// + /// Complete the transaction started by . + /// The pending changes (if any) to the item are discarded. + /// + public void CancelEdit() + { + if (IsAddingNew) + { + throw CollectionViewsError.ListCollectionView.MemberNotAllowedDuringTransaction("CancelEdit", "AddNew"); + } + + VerifyRefreshNotDeferred(); + + if (_editItem == null) + { + return; + } + + object editItem = _editItem; + SetEditItem(null); + + IEditableObject ieo = editItem as IEditableObject; + if (ieo != null) + { + ieo.CancelEdit(); + } + else + { + throw CollectionViewsError.ListCollectionView.CancelEditNotSupported(); + } + } + + private void ImplicitlyCancelEdit() + { + IEditableObject ieo = _editItem as IEditableObject; + SetEditItem(null); + + if (ieo != null) + { + ieo.CancelEdit(); + } + } + + /// + /// Gets a value indicating whether the view supports the notion of "pending changes" on the + /// current edit item. This may vary, depending on the view and the particular + /// item. For example, a view might return true if the current edit item + /// implements , or if the view has special + /// knowledge about the item that it can use to support rollback of pending + /// changes. + /// + public bool CanCancelEdit + { + get + { + return _canCancelEdit; + } + + private set + { + if (_canCancelEdit != value) + { + _canCancelEdit = value; + OnPropertyChanged(CanCancelEditPropertyName); + } + } + } + + private void RefreshCanCancelEdit() + { + CanCancelEdit = _editItem is IEditableObject; + } + + /// + /// Gets a value indicating whether an EditItem transaction is in progress. + /// + public bool IsEditingItem + { + get + { + return _editItem != null; + } + } + + /// + /// Gets the affected item when an EditItem" transaction is in progress. Otherwise it returns null. + /// + public object CurrentEditItem + { + get + { + return _editItem; + } + } + + private void SetEditItem(object item) + { + if (!object.Equals(item, _editItem)) + { + Debug.Assert(item == null || _editItem == null, "Old and new _editItem values are unexpectedly non null"); + _editItem = item; + + OnPropertyChanged(CurrentEditItemPropertyName); + OnPropertyChanged(IsEditingItemPropertyName); + RefreshCanCancelEdit(); + RefreshCanAddNew(); + RefreshCanRemove(); + } + } +#endif + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + /// + /// Handle CollectionChange events + /// + protected override void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) + { + if (args == null) + { + throw new ArgumentNullException("args"); + } + +#if DEBUG + Debug_ValidateCollectionChangedEventArgs(args); +#endif + +#if FEATURE_IEDITABLECOLLECTIONVIEW + // adding or replacing an item can change CanAddNew, by providing a + // non-null representative + if (!_isItemConstructorValid) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Replace: + RefreshCanAddNew(); + break; + } + } +#endif + int adjustedOldIndex = -1; + int adjustedNewIndex = -1; + + // If the Action is Reset then we do a Refresh. + if (args.Action == NotifyCollectionChangedAction.Reset) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + // implicitly cancel EditItem transactions + if (IsEditingItem) + { + ImplicitlyCancelEdit(); + } + + // adjust AddNew transactions, depending on whether the new item + // survived the Reset + if (IsAddingNew) + { + _newItemIndex = SourceList.IndexOf(_newItem); + if (_newItemIndex < 0) + { + EndAddNew(true); + } + } +#endif + RefreshOrDefer(); + + // the Refresh raises collection change event, so there's nothing left to do + return; + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (args.Action == NotifyCollectionChangedAction.Add && _newItemIndex == -2) + { + // The Add event came from AddNew. + BeginAddNew(args.NewItems[0], args.NewStartingIndex); + return; + } +#endif + + // If the Action is one that can be expected to have a valid NewItems[0] and NewStartingIndex then + // adjust the index for filtering and sorting. + if (args.Action != NotifyCollectionChangedAction.Remove) + { + adjustedNewIndex = AdjustBefore(NotifyCollectionChangedAction.Add, args.NewItems[0], args.NewStartingIndex); + } + + // If the Action is one that can be expected to have a valid OldItems[0] and OldStartingIndex then + // adjust the index for filtering and sorting. + if (args.Action != NotifyCollectionChangedAction.Add) + { + adjustedOldIndex = AdjustBefore(NotifyCollectionChangedAction.Remove, args.OldItems[0], args.OldStartingIndex); + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + // the new index needs further adjustment if the action removes (or moves) + // something before it. + if (UsesLocalArray && adjustedOldIndex >= 0 && adjustedOldIndex < adjustedNewIndex) + { + adjustedNewIndex--; + } +#endif + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + // handle interaction with AddNew and EditItem + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + if (args.NewStartingIndex <= _newItemIndex) + { + ++_newItemIndex; + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (args.OldStartingIndex < _newItemIndex) + { + --_newItemIndex; + } + + // implicitly cancel AddNew and/or EditItem transactions if the relevant item is removed + object item = args.OldItems[0]; + + if (item == CurrentEditItem) + { + ImplicitlyCancelEdit(); + } + else if (item == CurrentAddItem) + { + EndAddNew(true); + } + + break; + } +#endif + + ProcessCollectionChangedWithAdjustedIndex(args, adjustedOldIndex, adjustedNewIndex); + } + + private void ProcessCollectionChangedWithAdjustedIndex(NotifyCollectionChangedEventArgs args, int adjustedOldIndex, int adjustedNewIndex) + { + ProcessCollectionChangedWithAdjustedIndex( + (EffectiveNotifyCollectionChangedAction)args.Action, + (args.OldItems == null || args.OldItems.Count == 0) ? null : args.OldItems[0], + (args.NewItems == null || args.NewItems.Count == 0) ? null : args.NewItems[0], + adjustedOldIndex, + adjustedNewIndex); + } + + private void ProcessCollectionChangedWithAdjustedIndex(object movedItem, int adjustedOldIndex, int adjustedNewIndex) + { + ProcessCollectionChangedWithAdjustedIndex( + EffectiveNotifyCollectionChangedAction.Move, + movedItem, + movedItem, + adjustedOldIndex, + adjustedNewIndex); + } + + private void ProcessCollectionChangedWithAdjustedIndex(EffectiveNotifyCollectionChangedAction action, object oldItem, object newItem, int adjustedOldIndex, int adjustedNewIndex) + { + // Finding out the effective Action after filtering and sorting. + EffectiveNotifyCollectionChangedAction effectiveAction = action; + if (adjustedOldIndex == adjustedNewIndex && adjustedOldIndex >= 0) + { + effectiveAction = EffectiveNotifyCollectionChangedAction.Replace; + } + else if (adjustedOldIndex == -1) + { + // old index is unknown + // we weren't told the old index, but it may have been in the view. + if (adjustedNewIndex < 0) + { + // The new item will not be in the filtered view, + // so an Add is a no-op and anything else is a Remove. + if (action == EffectiveNotifyCollectionChangedAction.Add) + { + return; + } + + effectiveAction = EffectiveNotifyCollectionChangedAction.Remove; + } + } + else if (adjustedOldIndex < -1) + { + // old item is known to be NOT in filtered view + if (adjustedNewIndex < 0) + { + // since the old item wasn't in the filtered view, and the new + // item would not be in the filtered view, this is a no-op. + return; + } + else + { + effectiveAction = EffectiveNotifyCollectionChangedAction.Add; + } + } + else + { + // old item was in view + if (adjustedNewIndex < 0) + { + effectiveAction = EffectiveNotifyCollectionChangedAction.Remove; + } + else + { + effectiveAction = EffectiveNotifyCollectionChangedAction.Move; + } + } + + int originalCurrentPosition = CurrentPosition; + int oldCurrentPosition = CurrentPosition; + object oldCurrentItem = CurrentItem; + bool oldIsCurrentAfterLast = IsCurrentAfterLast; + bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + + // in the case of a replace that has a new adjustedPosition + // (likely caused by sorting), the only way to effectively communicate + // this change is through raising Remove followed by Insert. + NotifyCollectionChangedEventArgs args = null, args2 = null; + + switch (effectiveAction) + { + case EffectiveNotifyCollectionChangedAction.Add: + // insert into private view +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER +#if FEATURE_IEDITABLECOLLECTIONVIEW + // (unless it's a special item (i.e. new item)) + if (UsesLocalArray && (!IsAddingNew || !object.Equals(_newItem, newItem))) +#else + if (UsesLocalArray) +#endif + { + InternalList.Insert(adjustedNewIndex, newItem); + } +#endif + if (!IsGrouping) + { + AdjustCurrencyForAdd(adjustedNewIndex); + args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItem, adjustedNewIndex); + } +#if FEATURE_ICOLLECTIONVIEW_GROUP + else + { + AddItemToGroups(newItem); + } +#endif + break; + + case EffectiveNotifyCollectionChangedAction.Remove: +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + // remove from private view, unless it's not there to start with + // (e.g. when CommitNew is applied to an item that fails the filter) + if (UsesLocalArray) + { + int localOldIndex = adjustedOldIndex; + + if (localOldIndex < InternalList.Count && localOldIndex >= 0 && + object.Equals(InternalList[localOldIndex], oldItem)) + { + InternalList.RemoveAt(localOldIndex); + } + } +#endif + + if (!IsGrouping) + { + AdjustCurrencyForRemove(adjustedOldIndex); + args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, adjustedOldIndex); + } +#if FEATURE_ICOLLECTIONVIEW_GROUP + else + { + RemoveItemFromGroups(oldItem); + } +#endif + break; + + case EffectiveNotifyCollectionChangedAction.Replace: +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + // replace item in private view + if (UsesLocalArray) + { + InternalList[adjustedOldIndex] = newItem; + } +#endif + + if (!IsGrouping) + { + AdjustCurrencyForReplace(adjustedOldIndex); + args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, adjustedOldIndex); + } +#if FEATURE_ICOLLECTIONVIEW_GROUP + else + { + RemoveItemFromGroups(oldItem); + AddItemToGroups(newItem); + } +#endif + break; + + case EffectiveNotifyCollectionChangedAction.Move: + // remove from private view +#if FEATURE_ICOLLECTIONVIEW_GROUP + bool simpleMove = oldItem == newItem; +#endif +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + if (UsesLocalArray) + { + int localOldIndex = adjustedOldIndex; + int localNewIndex = adjustedNewIndex; + + // remove the item from its old position, unless it's not there + // (which happens when the item is the object of CommitNew) + if (localOldIndex < InternalList.Count && localOldIndex >= 0 && + object.Equals(InternalList[localOldIndex], oldItem)) + { + InternalList.RemoveAt(localOldIndex); + } + + // put the item into its new position + InternalList.Insert(localNewIndex, newItem); + } +#endif + + if (!IsGrouping) + { + AdjustCurrencyForMove(adjustedOldIndex, adjustedNewIndex); + + // move/replace + args2 = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItem, adjustedNewIndex); + args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, adjustedOldIndex); + } +#if FEATURE_ICOLLECTIONVIEW_GROUP + else if (!simpleMove) + { + RemoveItemFromGroups(oldItem); + AddItemToGroups(newItem); + } +#endif + break; + + default: + Debug.Assert(false, "Unexpected Effective Collection Change Action"); + break; + } + + // remember whether scalar properties of the view have changed. + // They may change again during the collection change event, so we + // need to do the test before raising that event. + bool afterLastHasChanged = IsCurrentAfterLast != oldIsCurrentAfterLast; + bool beforeFirstHasChanged = IsCurrentBeforeFirst != oldIsCurrentBeforeFirst; + bool currentPositionHasChanged = CurrentPosition != oldCurrentPosition; + bool currentItemHasChanged = CurrentItem != oldCurrentItem; + + // take a new snapshot of the scalar properties, so that we can detect + // changes made during the collection change event + oldIsCurrentAfterLast = IsCurrentAfterLast; + oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + oldCurrentPosition = CurrentPosition; + oldCurrentItem = CurrentItem; + + // base class will raise an event to our listeners + if (!IsGrouping) + { + // we've already returned if (action == NotifyCollectionChangedAction.Reset) above + + // To avoid notification reentrancy we need to mark this collection view as processing a change + // so any changes to the current item will only be raised once, and from this method + // _currentChangedMonitor is used to guard whether the CurrentChanged and CurrentChanging event can be fired + // so by entering it we're preventing the base calls from firing those events. + Debug.Assert(!CurrentChangedMonitor.Busy, "Expected _currentChangedMonitor.Busy is false."); + + CurrentChangedMonitor.Enter(); + using (CurrentChangedMonitor) + { + OnCollectionChanged(args); + if (args2 != null) + { + OnCollectionChanged(args2); + } + } + + // Any scalar properties that changed don't need a further notification, + // but do need a new snapshot + if (IsCurrentAfterLast != oldIsCurrentAfterLast) + { + afterLastHasChanged = false; + oldIsCurrentAfterLast = IsCurrentAfterLast; + } + + if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) + { + beforeFirstHasChanged = false; + oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; + } + + if (CurrentPosition != oldCurrentPosition) + { + currentPositionHasChanged = false; + oldCurrentPosition = CurrentPosition; + } + + if (CurrentItem != oldCurrentItem) + { + currentItemHasChanged = false; + oldCurrentItem = CurrentItem; + } + } + + // currency has to change after firing the deletion event, + // so event handlers have the right picture + if (_currentElementWasRemoved) + { + int oldCurPos = originalCurrentPosition; + +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (_newGroupedItem != null) + { + oldCurPos = IndexOf(_newGroupedItem); + } +#endif + MoveCurrencyOffDeletedElement(oldCurPos); + + // changes to the scalar properties need notification + afterLastHasChanged = afterLastHasChanged || (IsCurrentAfterLast != oldIsCurrentAfterLast); + beforeFirstHasChanged = beforeFirstHasChanged || (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); + currentPositionHasChanged = currentPositionHasChanged || (CurrentPosition != oldCurrentPosition); + currentItemHasChanged = currentItemHasChanged || (CurrentItem != oldCurrentItem); + } + + // notify that the properties have changed. We may end up doing + // double notification for properties that change during the collection + // change event, but that's not harmful. Detecting the double change + // is more trouble than it's worth. + RaiseCurrencyChanges( + false /*raiseCurrentChanged*/, + currentItemHasChanged, + currentPositionHasChanged, + beforeFirstHasChanged, + afterLastHasChanged); + } + + /// + /// Return index of item in the internal list. + /// + /// Index of item in the internal list. + protected int InternalIndexOf(object item) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (IsGrouping) + { + return _group.LeafIndexOf(item); + } +#endif +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (IsAddingNew && object.Equals(item, _newItem) && UsesLocalArray) + { + return InternalCount - 1; + } +#endif +#endif + return InternalList.IndexOf(item); + } + + /// + /// Return item at the given index in the internal list. + /// + /// Item at the given index in the internal list. + protected object InternalItemAt(int index) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (IsGrouping) + { + return _group.LeafAt(index); + } +#endif +#if FEATURE_IEDITABLECOLLECTIONVIEW +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + if (IsAddingNew && UsesLocalArray && index == InternalCount - 1) + { + return _newItem; + } +#endif +#endif + return InternalList[index]; + } + + /// + /// Return true if internal list contains the item. + /// + /// True if internal list contains the item. + protected bool InternalContains(object item) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (!IsGrouping) + { +#endif + return InternalList.Contains(item); +#if FEATURE_ICOLLECTIONVIEW_GROUP + } + else + { + return _group.LeafIndexOf(item) >= 0; + } +#endif + } + + /// + /// Return an enumerator for the internal list. + /// + /// An enumerator for the internal list. + protected IEnumerator InternalGetEnumerator() + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (!IsGrouping) + { +#endif +#if FEATURE_IEDITABLECOLLECTIONVIEW + return new PlaceholderAwareEnumerator(this, InternalList.GetEnumerator(), _newItem); +#else + return new PlaceholderAwareEnumerator(this, InternalList.GetEnumerator(), NoNewItem); +#endif +#if FEATURE_ICOLLECTIONVIEW_GROUP + } + else + { + return _group.GetLeafEnumerator(); + } +#endif + } + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + /// + /// Gets a value indicating whether a private copy of the data is needed for sorting and filtering + /// + protected bool UsesLocalArray + { + get + { + return ActiveComparer != null || ActiveFilter != null; + } + } +#endif + + /// + /// Gets a protected accessor to private _internalList field. + /// + protected IList InternalList + { + get + { + return _internalList; + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Gets or sets a protected accessor to private _activeComparer field. + /// + protected IComparer ActiveComparer + { + get + { + return _activeComparer; + } + + set + { + _activeComparer = value; + } + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_FILTER + /// + /// Gets or sets a protected accessor to private _activeFilter field. + /// + protected Predicate ActiveFilter + { + get + { + return _activeFilter; + } + + set + { + _activeFilter = value; + } + } +#endif + + /// + /// Gets a value indicating whether grouping is supported. + /// + protected bool IsGrouping + { + get + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + return _isGrouping; +#else + return false; +#endif + } + } + + /// + /// Gets a protected accessor to private count. + /// + protected int InternalCount + { + get + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (IsGrouping) + { + return _group.ItemCount; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + bool usesLocalArray = UsesLocalArray; +#else + bool usesLocalArray = false; +#endif +#if FEATURE_IEDITABLECOLLECTIONVIEW + bool isAddingNew = IsAddingNew; +#else + bool isAddingNew = false; +#endif + int delta = (usesLocalArray && isAddingNew) ? 1 : 0; + return delta + InternalList.Count; + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + // returns true if this ListCollectionView has sort descriptions, + // without tripping off lazy creation of SortDescriptions collection. + internal bool HasSortDescriptions + { + get + { + return _sort != null && _sort.Count > 0; + } + } +#endif + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + // true if CurrentPosition points to item within view + private bool IsCurrentInView + { + get + { + return CurrentPosition >= 0 && CurrentPosition < InternalCount; + } + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + // can the group name(s) for an item change after we've grouped the item? + private bool CanGroupNamesChange + { + // There's no way we can deduce this - the app has to tell us. + // If this is true, removing a grouped item is quite difficult. + // We cannot rely on its group names to tell us which group we inserted + // it into (they may have been different at insertion time), so we + // have to do a linear search. + get + { + return true; + } + } +#endif + + private IList SourceList + { + get + { + return SourceCollection as IList; + } + } + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + /// + /// Validates provided NotifyCollectionChangedEventArgs + /// + /// NotifyCollectionChangedEventArgs to validate. + [Conditional("DEBUG")] + private void Debug_ValidateCollectionChangedEventArgs(NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Debug.Assert(e.NewItems.Count == 1, "Unexpected NotifyCollectionChangedEventArgs.NewItems.Count for Add action"); + break; + + case NotifyCollectionChangedAction.Remove: + Debug.Assert(e.OldItems.Count == 1, "Unexpected NotifyCollectionChangedEventArgs.OldItems.Count for Remove action"); + break; + + case NotifyCollectionChangedAction.Replace: + Debug.Assert(e.OldItems.Count == 1, "Unexpected NotifyCollectionChangedEventArgs.OldItems.Count for Replace action"); + Debug.Assert(e.NewItems.Count == 1, "Unexpected NotifyCollectionChangedEventArgs.NewItems.Count for Replace action"); + break; + + case NotifyCollectionChangedAction.Reset: + break; + + default: + Debug.Assert(false, "Unexpected NotifyCollectionChangedEventArgs action"); + break; + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + /// + /// Create, filter and sort the local index array. + /// called from Refresh(), override in derived classes as needed. + /// + /// new ILIst to associate this view with + /// new local array to use for this view + private IList PrepareLocalArray(IList list) + { + if (list == null) + { + throw new ArgumentNullException("list"); + } + + // filter the collection's array into the local array + List al; + + if (ActiveFilter == null) + { + al = new List(); + foreach (var o in list) + { + al.Add(o); + } + } + else + { + al = new List(list.Count); + for (int k = 0; k < list.Count; ++k) + { + if (ActiveFilter(list[k])) + { + al.Add(list[k]); + } + } + } + + // sort the local array + if (ActiveComparer != null) + { + al.Sort(ActiveComparer); + } + + return al; + } +#endif + + private void MoveCurrencyOffDeletedElement(int oldCurrentPosition) + { + int lastPosition = InternalCount - 1; // OK if last is -1 + + // if position falls beyond last position, move back to last position + int newPosition = (oldCurrentPosition < lastPosition) ? oldCurrentPosition : lastPosition; + + // reset this to false before raising events to avoid problems in re-entrancy + _currentElementWasRemoved = false; + + OnCurrentChanging(); + + if (newPosition < 0) + { + SetCurrent(null, newPosition); + } + else + { + SetCurrent(InternalItemAt(newPosition), newPosition); + } + + OnCurrentChanged(); + } + + // Convert the collection's index to an index into the view. + // Return -1 if the index is unknown or moot (Reset events). + // Return -2 if the event doesn't apply to this view. + private int AdjustBefore(NotifyCollectionChangedAction action, object item, int index) + { + // index is not relevant to Reset events + if (action == NotifyCollectionChangedAction.Reset) + { + return -1; + } + + IList ilFull = SourceCollection as IList; + + // validate input + if (index < -1 || index > ilFull.Count) + { + throw CollectionViewsError.ListCollectionView.CollectionChangedOutOfRange(); + } + + if (action == NotifyCollectionChangedAction.Add) + { + if (index >= 0) + { + if (!object.Equals(item, ilFull[index])) + { + throw CollectionViewsError.CollectionView.ItemNotAtIndex("added"); + } + } + else + { + // event didn't specify index - determine it the hard way + index = ilFull.IndexOf(item); + if (index < 0) + { + throw CollectionViewsError.ListCollectionView.AddedItemNotInCollection(); + } + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + // If there's no sort or filter, use the index into the full array + if (!UsesLocalArray) +#endif + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (IsAddingNew) + { + if (index > _newItemIndex) + { + index--; // the new item has been artificially moved elsewhere + } + } +#endif + return index; + } + +#if FEATURE_ICOLLECTIONVIEW_SORT_OR_FILTER + if (action == NotifyCollectionChangedAction.Add) + { +#if FEATURE_ICOLLECTIONVIEW_FILTER + // if the item isn't in the filter, return -2 + if (!PassesFilter(item)) + { + return -2; + } +#endif + // search the local array + List al = InternalList as List; + if (al == null) + { + index = -1; + } +#if FEATURE_ICOLLECTIONVIEW_SORT + else if (ActiveComparer != null) + { + // if there's a sort order, use binary search + index = al.BinarySearch(item, ActiveComparer); + if (index < 0) + { + index = ~index; + } + } +#endif + else + { + // otherwise, do a linear search of the full array, advancing + // localIndex past elements that appear in the local array, + // until either (a) reaching the position of the item in the + // full array, or (b) falling off the end of the local array. + // localIndex is now the desired index. + // One small wrinkle: we have to ignore the target item in + // the local array (this arises in a Move event). + int fullIndex = 0, localIndex = 0; + + while (fullIndex < index && localIndex < al.Count) + { + if (object.Equals(ilFull[fullIndex], al[localIndex])) + { + // match - current item passes filter. Skip it. + ++fullIndex; + ++localIndex; + } + else if (object.Equals(item, al[localIndex])) + { + // skip over an unmatched copy of the target item + // (this arises in a Move event) + ++localIndex; + } + else + { + // no match - current item fails filter. Ignore it. + ++fullIndex; + } + } + + index = localIndex; + } + } + else if (action == NotifyCollectionChangedAction.Remove) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (!IsAddingNew || item != _newItem) + { +#endif + // a deleted item should already be in the local array + index = InternalList.IndexOf(item); + + // but may not be, if it was already filtered out (can't use + // PassesFilter here, because the item could have changed + // while it was out of our sight) + if (index < 0) + { + return -2; + } +#if FEATURE_IEDITABLECOLLECTIONVIEW + } + else + { + // the new item is in a special position + return InternalCount - 1; + } +#endif + } + else + { + index = -1; + } + + return (index < 0) ? index : index; +#endif + } + + // fix up CurrentPosition and CurrentItem after a collection change + private void AdjustCurrencyForAdd(int index) + { + if (InternalCount == 1) + { + if (CurrentItem != null || CurrentPosition != -1) + { + // fire current changing notification + OnCurrentChanging(); + } + + // added first item; set current at BeforeFirst + SetCurrent(null, -1); + } + else if (index <= CurrentPosition) + { + // adjust current index if insertion is earlier + int newPosition = CurrentPosition + 1; + + if (newPosition < InternalCount) + { + // CurrentItem might be out of sync if underlying list is not INCC + // or if this Add is the result of a Replace (Rem + Add) + SetCurrent(GetItemAt(newPosition), newPosition); + } + else + { + SetCurrent(null, InternalCount); + } + } + else if (!IsCurrentInSync) + { + // Make sure current item and position are in sync. + SetCurrent(CurrentItem, InternalIndexOf(CurrentItem)); + } + } + + // fix up CurrentPosition and CurrentItem after a collection change + private void AdjustCurrencyForRemove(int index) + { + // adjust current index if deletion is earlier + if (index < CurrentPosition) + { + SetCurrent(CurrentItem, CurrentPosition - 1); + } + + // remember to move currency off the deleted element + else if (index == CurrentPosition) + { + _currentElementWasRemoved = true; + } + } + + // fix up CurrentPosition and CurrentItem after a collection change + private void AdjustCurrencyForMove(int oldIndex, int newIndex) + { + if (oldIndex == CurrentPosition) + { + // moving the current item - currency moves with the item (bug 1942184) + SetCurrent(GetItemAt(newIndex), newIndex); + } + else if (oldIndex < CurrentPosition && CurrentPosition <= newIndex) + { + // moving an item from before current position to after - + // current item shifts back one position + SetCurrent(CurrentItem, CurrentPosition - 1); + } + else if (newIndex <= CurrentPosition && CurrentPosition < oldIndex) + { + // moving an item from after current position to before - + // current item shifts ahead one position + SetCurrent(CurrentItem, CurrentPosition + 1); + } + + // else no change necessary + } + + // fix up CurrentPosition and CurrentItem after a collection change + private void AdjustCurrencyForReplace(int index) + { + // remember to move currency off the deleted element + if (index == CurrentPosition) + { + _currentElementWasRemoved = true; + } + } + + private void RaiseCurrencyChanges( + bool raiseCurrentChanged, + bool raiseCurrentItem, + bool raiseCurrentPosition, + bool raiseIsCurrentBeforeFirst, + bool raiseIsCurrentAfterLast) + { + if (raiseCurrentChanged) + { + OnCurrentChanged(); + } + + if (raiseIsCurrentAfterLast) + { + OnPropertyChanged(IsCurrentAfterLastPropertyName); + } + + if (raiseIsCurrentBeforeFirst) + { + OnPropertyChanged(IsCurrentBeforeFirstPropertyName); + } + + if (raiseCurrentPosition) + { + OnPropertyChanged(CurrentPositionPropertyName); + } + + if (raiseCurrentItem) + { + OnPropertyChanged(CurrentItemPropertyName); + } + } + + // build the sort and filter information from the relevant properties + private void PrepareSortAndFilter() + { +#if FEATURE_ICOLLECTIONVIEW_SORT + // sort: prepare the comparer + if (_sort != null && _sort.Count > 0) + { + ActiveComparer = new SortFieldComparer(_sort, Culture); + } + else + { + ActiveComparer = null; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_FILTER + // filter: prepare the Predicate filter + ActiveFilter = Filter; +#endif + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + // set new SortDescription collection; rehook collection change notification handler + private void SetSortDescriptions(SortDescriptionCollection descriptions) + { + if (_sort != null) + { + ((INotifyCollectionChanged)_sort).CollectionChanged -= new NotifyCollectionChangedEventHandler(SortDescriptionsChanged); + } + + _sort = descriptions; + + if (_sort != null) + { + Debug.Assert(_sort.Count == 0, "must be empty SortDescription collection"); + ((INotifyCollectionChanged)_sort).CollectionChanged += new NotifyCollectionChangedEventHandler(SortDescriptionsChanged); + } + } + + // SortDescription was added/removed, refresh CollectionView + private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (IsAddingNew || IsEditingItem) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("Sorting"); + } +#endif + RefreshOrDefer(); + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_GROUP + // divide the data items into groups + private void PrepareGroups() + { + // discard old groups + _group.Clear(); + + // initialize the synthetic top level group + _group.Initialize(); + + // if there's no grouping, there's nothing to do + _isGrouping = _group.GroupBy != null; + if (!_isGrouping) + { + return; + } + + // reset the grouping comparer + IComparer comparer = ActiveComparer; + if (comparer != null) + { + _group.ActiveComparer = comparer; + } + else + { + CollectionViewGroupInternal.IListComparer ilc = _group.ActiveComparer as CollectionViewGroupInternal.IListComparer; + if (ilc != null) + { + ilc.ResetList(InternalList); + } + else + { + _group.ActiveComparer = new CollectionViewGroupInternal.IListComparer(InternalList); + } + } + + // loop through the sorted/filtered list of items, dividing them + // into groups (with special cases for new item) + for (int k = 0, n = InternalList.Count; k < n; ++k) + { + object item = InternalList[k]; + if (!IsAddingNew || !object.Equals(_newItem, item)) + { + _group.AddToSubgroups(item, true /*loading*/); + } + } + + if (IsAddingNew) + { + _group.InsertSpecialItem(_group.Items.Count, _newItem, true /*loading*/); + } + } + + // For the Group to report collection changed + private void OnGroupChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + AdjustCurrencyForAdd(e.NewStartingIndex); + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + AdjustCurrencyForRemove(e.OldStartingIndex); + } + + OnCollectionChanged(e); + } + + // The GroupDescriptions collection changed + private void OnGroupByChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (IsAddingNew || IsEditingItem) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("Grouping"); + } + + // This is a huge change. Just refresh the view. + RefreshOrDefer(); + } + + // A group description for one of the subgroups changed + private void OnGroupDescriptionChanged(object sender, EventArgs e) + { + if (IsAddingNew || IsEditingItem) + { + throw CollectionViewsError.CollectionView.MemberNotAllowedDuringAddOrEdit("Grouping"); + } + + // This is a huge change. Just refresh the view. + RefreshOrDefer(); + } + + // An item was inserted into the collection. Update the groups. + private void AddItemToGroups(object item) + { + if (IsAddingNew && item == _newItem) + { + _group.InsertSpecialItem(_group.Items.Count, item, false /*loading*/); + } + else + { + _group.AddToSubgroups(item, false /*loading*/); + } + } + + // An item was removed from the collection. Update the groups. + private void RemoveItemFromGroups(object item) + { + if (CanGroupNamesChange || _group.RemoveFromSubgroups(item)) + { + // the item didn't appear where we expected it to. + _group.RemoveItemFromSubgroupsByExhaustiveSearch(item); + } + } +#endif + + /// + /// Helper to raise a PropertyChanged event />). + /// + private void OnPropertyChanged(string propertyName) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + //------------------------------------------------------ + // + // Private Types + // + //------------------------------------------------------ + + /// + /// Private EffectiveNotifyCollectionChangedAction enum + /// + private enum EffectiveNotifyCollectionChangedAction + { + Add = 0, + Remove = 1, + Replace = 2, + Move = 3, + Reset = 4 + } + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + private IList _internalList; + private bool _currentElementWasRemoved; // true if we need to MoveCurrencyOffDeletedElement +#if FEATURE_ICOLLECTIONVIEW_FILTER + private Predicate _activeFilter; +#endif +#if FEATURE_ICOLLECTIONVIEW_SORT + private IComparer _activeComparer; + private SortDescriptionCollection _sort; +#endif +#if FEATURE_ICOLLECTIONVIEW_GROUP + private object _newGroupedItem; // used when a CommitNew is called in grouping scenarios + private CollectionViewGroupRoot _group; + private bool _isGrouping; +#endif +#if FEATURE_IEDITABLECOLLECTIONVIEW + private bool _canAddNew; + private bool _canRemove; + private bool _canCancelEdit; + private bool _isItemConstructorValid; + private ConstructorInfo _itemConstructor; + private object _editItem; + private object _newItem = NoNewItem; + private int _newItemIndex; // position _newItem in the source collection +#endif + +#if FEATURE_IEDITABLECOLLECTIONVIEW + //------------------------------------------------------ + // + // Constants + // + //------------------------------------------------------ + + // ListCollectionView-specific property names, introduced by IEditableCollectionView + internal const string CanAddNewPropertyName = "CanAddNew"; + internal const string CanCancelEditPropertyName = "CanCancelEdit"; + internal const string CanRemovePropertyName = "CanRemove"; + internal const string CurrentAddItemPropertyName = "CurrentAddItem"; + internal const string CurrentEditItemPropertyName = "CurrentEditItem"; + internal const string IsAddingNewPropertyName = "IsAddingNew"; + internal const string IsEditingItemPropertyName = "IsEditingItem"; +#endif + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridAutomationPeer.cs new file mode 100644 index 0000000..3e0bd9e --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridAutomationPeer.cs @@ -0,0 +1,988 @@ +// 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.Generic; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// Exposes types to UI Automation. + /// + public class DataGridAutomationPeer : + FrameworkElementAutomationPeer, + IGridProvider, + IScrollProvider, + ISelectionProvider, + ITableProvider + { + private Dictionary _groupItemPeers = new Dictionary(); + private Dictionary _itemPeers = new Dictionary(); + private bool _oldHorizontallyScrollable; + private double _oldHorizontalScrollPercent; + private double _oldHorizontalViewSize; + private bool _oldVerticallyScrollable; + private double _oldVerticalScrollPercent; + private double _oldVerticalViewSize; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The that is associated with this . + /// + public DataGridAutomationPeer(DataGrid owner) + : base(owner) + { + if (this.HorizontallyScrollable) + { + _oldHorizontallyScrollable = true; + _oldHorizontalScrollPercent = this.HorizontalScrollPercent; + _oldHorizontalViewSize = this.HorizontalViewSize; + } + else + { + _oldHorizontallyScrollable = false; + _oldHorizontalScrollPercent = ScrollPatternIdentifiers.NoScroll; + _oldHorizontalViewSize = 100.0; + } + + if (this.VerticallyScrollable) + { + _oldVerticallyScrollable = true; + _oldVerticalScrollPercent = this.VerticalScrollPercent; + _oldVerticalViewSize = this.VerticalViewSize; + } + else + { + _oldVerticallyScrollable = false; + _oldVerticalScrollPercent = ScrollPatternIdentifiers.NoScroll; + _oldVerticalViewSize = 100.0; + } + } + + private bool HorizontallyScrollable + { + get + { + return OwningDataGrid.HorizontalScrollBar != null && OwningDataGrid.HorizontalScrollBar.Maximum > 0; + } + } + + private double HorizontalScrollPercent + { + get + { + if (!this.HorizontallyScrollable) + { + return ScrollPatternIdentifiers.NoScroll; + } + + return (double)(this.OwningDataGrid.HorizontalScrollBar.Value * 100.0 / this.OwningDataGrid.HorizontalScrollBar.Maximum); + } + } + + private double HorizontalViewSize + { + get + { + if (!this.HorizontallyScrollable || DoubleUtil.IsZero(this.OwningDataGrid.HorizontalScrollBar.Maximum)) + { + return 100.0; + } + + return (double)(this.OwningDataGrid.HorizontalScrollBar.ViewportSize * 100.0 / + (this.OwningDataGrid.HorizontalScrollBar.ViewportSize + this.OwningDataGrid.HorizontalScrollBar.Maximum)); + } + } + + private DataGrid OwningDataGrid + { + get + { + return Owner as DataGrid; + } + } + + private bool VerticallyScrollable + { + get + { + return OwningDataGrid.VerticalScrollBar != null && OwningDataGrid.VerticalScrollBar.Maximum > 0; + } + } + + private double VerticalScrollPercent + { + get + { + if (!this.VerticallyScrollable) + { + return ScrollPatternIdentifiers.NoScroll; + } + + return (double)(this.OwningDataGrid.VerticalScrollBar.Value * 100.0 / this.OwningDataGrid.VerticalScrollBar.Maximum); + } + } + + private double VerticalViewSize + { + get + { + if (!this.VerticallyScrollable || DoubleUtil.IsZero(this.OwningDataGrid.VerticalScrollBar.Maximum)) + { + return 100.0; + } + + return (double)(this.OwningDataGrid.VerticalScrollBar.ViewportSize * 100.0 / + (this.OwningDataGrid.VerticalScrollBar.ViewportSize + this.OwningDataGrid.VerticalScrollBar.Maximum)); + } + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.DataGrid; + } + + /// + /// Gets the collection of elements that are represented in the UI Automation tree as immediate + /// child elements of the automation peer. + /// + /// The children elements. + protected override IList GetChildrenCore() + { + IList children = base.GetChildrenCore(); + if (this.OwningDataGrid != null) + { + children.Remove(ScrollBarAutomationPeer.FromElement(this.OwningDataGrid.HorizontalScrollBar)); + children.Remove(ScrollBarAutomationPeer.FromElement(this.OwningDataGrid.VerticalScrollBar)); + } + + return children; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Called by GetName. + /// + /// + /// Returns the first of these that is not null or empty: + /// - Value returned by the base implementation + /// - Name of the owning DataGrid + /// - DataGrid class name + /// + protected override string GetNameCore() + { + string name = base.GetNameCore(); + if (string.IsNullOrEmpty(name)) + { + if (this.OwningDataGrid != null) + { + name = this.OwningDataGrid.Name; + } + + if (string.IsNullOrEmpty(name)) + { + name = this.GetClassName(); + } + } + +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridAutomationPeer.GetNameCore returns " + name); +#endif + + return name; + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + switch (patternInterface) + { + case PatternInterface.Grid: + case PatternInterface.Selection: + case PatternInterface.Table: + return this; + case PatternInterface.Scroll: + { + if (this.HorizontallyScrollable || this.VerticallyScrollable) + { + return this; + } + + break; + } + } + + return base.GetPatternCore(patternInterface); + } + + int IGridProvider.ColumnCount + { + get + { + return this.OwningDataGrid.Columns.Count; + } + } + + int IGridProvider.RowCount + { + get + { + return this.OwningDataGrid.DataConnection.Count; + } + } + + IRawElementProviderSimple IGridProvider.GetItem(int row, int column) + { + if (this.OwningDataGrid != null && + this.OwningDataGrid.DataConnection != null && + row >= 0 && row < this.OwningDataGrid.SlotCount && + column >= 0 && column < this.OwningDataGrid.Columns.Count) + { + object item = null; + if (!this.OwningDataGrid.IsSlotVisible(this.OwningDataGrid.SlotFromRowIndex(row))) + { + item = this.OwningDataGrid.DataConnection.GetDataItem(row); + } + + this.OwningDataGrid.ScrollIntoView(item, this.OwningDataGrid.Columns[column]); + + DataGridRow dgr = this.OwningDataGrid.DisplayData.GetDisplayedRow(row); + if (this.OwningDataGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + column++; + } + + Debug.Assert(column >= 0, "Expected positive column value."); + Debug.Assert(column < this.OwningDataGrid.ColumnsItemsInternal.Count, "Expected smaller column value."); + DataGridCell cell = dgr.Cells[column]; + AutomationPeer peer = CreatePeerForElement(cell); + if (peer != null) + { + return ProviderFromPeer(peer); + } + } + + return null; + } + + bool IScrollProvider.HorizontallyScrollable + { + get + { + return this.HorizontallyScrollable; + } + } + + double IScrollProvider.HorizontalScrollPercent + { + get + { + return this.HorizontalScrollPercent; + } + } + + double IScrollProvider.HorizontalViewSize + { + get + { + return this.HorizontalViewSize; + } + } + + bool IScrollProvider.VerticallyScrollable + { + get + { + return this.VerticallyScrollable; + } + } + + double IScrollProvider.VerticalScrollPercent + { + get + { + return this.VerticalScrollPercent; + } + } + + double IScrollProvider.VerticalViewSize + { + get + { + return this.VerticalViewSize; + } + } + + void IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount) + { + if (!IsEnabled()) + { + throw new ElementNotEnabledException(); + } + + bool scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount; + bool scrollVertically = verticalAmount != ScrollAmount.NoAmount; + + if ((scrollHorizontally && !this.HorizontallyScrollable) || (scrollVertically && !this.VerticallyScrollable)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + switch (horizontalAmount) + { + // In the small increment and decrement calls, ScrollHorizontally will adjust the + // ScrollBar.Value itself, so we don't need to do it here + case ScrollAmount.SmallIncrement: + this.OwningDataGrid.ProcessHorizontalScroll(ScrollEventType.SmallIncrement); + break; + case ScrollAmount.LargeIncrement: + this.OwningDataGrid.HorizontalScrollBar.Value += this.OwningDataGrid.HorizontalScrollBar.LargeChange; + this.OwningDataGrid.ProcessHorizontalScroll(ScrollEventType.LargeIncrement); + break; + case ScrollAmount.SmallDecrement: + this.OwningDataGrid.ProcessHorizontalScroll(ScrollEventType.SmallDecrement); + break; + case ScrollAmount.LargeDecrement: + this.OwningDataGrid.HorizontalScrollBar.Value -= this.OwningDataGrid.HorizontalScrollBar.LargeChange; + this.OwningDataGrid.ProcessHorizontalScroll(ScrollEventType.LargeDecrement); + break; + case ScrollAmount.NoAmount: + break; + default: + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + switch (verticalAmount) + { + // In the small increment and decrement calls, ScrollVertically will adjust the + // ScrollBar.Value itself, so we don't need to do it here + case ScrollAmount.SmallIncrement: + this.OwningDataGrid.ProcessVerticalScroll(ScrollEventType.SmallIncrement); + break; + case ScrollAmount.LargeIncrement: + this.OwningDataGrid.VerticalScrollBar.Value += this.OwningDataGrid.VerticalScrollBar.LargeChange; + this.OwningDataGrid.ProcessVerticalScroll(ScrollEventType.LargeIncrement); + break; + case ScrollAmount.SmallDecrement: + this.OwningDataGrid.ProcessVerticalScroll(ScrollEventType.SmallDecrement); + break; + case ScrollAmount.LargeDecrement: + this.OwningDataGrid.VerticalScrollBar.Value -= this.OwningDataGrid.VerticalScrollBar.LargeChange; + this.OwningDataGrid.ProcessVerticalScroll(ScrollEventType.LargeDecrement); + break; + case ScrollAmount.NoAmount: + break; + default: + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + void IScrollProvider.SetScrollPercent(double horizontalPercent, double verticalPercent) + { + if (!IsEnabled()) + { + throw new ElementNotEnabledException(); + } + + bool scrollHorizontally = horizontalPercent != (double)ScrollPatternIdentifiers.NoScroll; + bool scrollVertically = verticalPercent != (double)ScrollPatternIdentifiers.NoScroll; + + if ((scrollHorizontally && !this.HorizontallyScrollable) || (scrollVertically && !this.VerticallyScrollable)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + if (scrollHorizontally && (horizontalPercent < 0.0 || horizontalPercent > 100.0)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + if (scrollVertically && (verticalPercent < 0.0 || verticalPercent > 100.0)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + if (scrollHorizontally) + { + this.OwningDataGrid.HorizontalScrollBar.Value = + (double)(this.OwningDataGrid.HorizontalScrollBar.Maximum * (horizontalPercent / 100.0)); + this.OwningDataGrid.ProcessHorizontalScroll(ScrollEventType.ThumbPosition); + } + + if (scrollVertically) + { + this.OwningDataGrid.VerticalScrollBar.Value = + (double)(this.OwningDataGrid.VerticalScrollBar.Maximum * (verticalPercent / 100.0)); + this.OwningDataGrid.ProcessVerticalScroll(ScrollEventType.ThumbPosition); + } + } + + IRawElementProviderSimple[] ISelectionProvider.GetSelection() + { + if (this.OwningDataGrid != null && + this.OwningDataGrid.SelectedItems != null) + { + List selectedProviders = new List(); + foreach (object item in this.OwningDataGrid.SelectedItems) + { + DataGridItemAutomationPeer peer = GetOrCreateItemPeer(item); + if (peer != null) + { + selectedProviders.Add(ProviderFromPeer(peer)); + } + } + + return selectedProviders.ToArray(); + } + + return null; + } + + bool ISelectionProvider.CanSelectMultiple + { + get + { + return this.OwningDataGrid.SelectionMode == DataGridSelectionMode.Extended; + } + } + + bool ISelectionProvider.IsSelectionRequired + { + get + { + return false; + } + } + + RowOrColumnMajor ITableProvider.RowOrColumnMajor + { + get + { + return RowOrColumnMajor.RowMajor; + } + } + + IRawElementProviderSimple[] ITableProvider.GetColumnHeaders() + { + if (this.OwningDataGrid.AreColumnHeadersVisible) + { + List providers = new List(); + foreach (DataGridColumn column in this.OwningDataGrid.Columns) + { + if (column.HeaderCell != null) + { + AutomationPeer peer = CreatePeerForElement(column.HeaderCell); + if (peer != null) + { + providers.Add(ProviderFromPeer(peer)); + } + } + } + + if (providers.Count > 0) + { + return providers.ToArray(); + } + } + + return null; + } + + IRawElementProviderSimple[] ITableProvider.GetRowHeaders() + { + if (this.OwningDataGrid.AreRowHeadersVisible) + { + List providers = new List(); + foreach (DataGridRow row in this.OwningDataGrid.DisplayData.GetScrollingElements(true /*onlyRows*/)) + { + if (row.HeaderCell != null) + { + AutomationPeer peer = CreatePeerForElement(row.HeaderCell); + if (peer != null) + { + providers.Add(ProviderFromPeer(peer)); + } + } + } + + if (providers.Count > 0) + { + return providers.ToArray(); + } + } + + return null; + } + + private AutomationPeer GetCellPeer(int slot, int column) + { + if (slot >= 0 && slot < this.OwningDataGrid.SlotCount && + column >= 0 && column < this.OwningDataGrid.ColumnsItemsInternal.Count && + this.OwningDataGrid.IsSlotVisible(slot)) + { + DataGridRow row = this.OwningDataGrid.DisplayData.GetDisplayedElement(slot) as DataGridRow; + if (row != null) + { + Debug.Assert(column >= 0, "Expected positive column value."); + Debug.Assert(column < this.OwningDataGrid.ColumnsItemsInternal.Count, "Expected smaller column value."); + DataGridCell cell = row.Cells[column]; + return CreatePeerForElement(cell); + } + } + + return null; + } + + internal static void RaiseAutomationInvokeEvent(UIElement element) + { + if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked)) + { + AutomationPeer peer = FrameworkElementAutomationPeer.FromElement(element); + if (peer != null) + { +#if DEBUG_AUTOMATION + Debug.WriteLine(peer.ToString() + ".RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked); + } + } + } + + internal List GetChildPeers() + { + List peers = new List(); + PopulateGroupItemPeers(); + PopulateItemPeers(); + if (_groupItemPeers != null && _groupItemPeers.Count > 0) + { + foreach (object group in this.OwningDataGrid.DataConnection.CollectionView.CollectionGroups /*Groups*/) + { + peers.Add(_groupItemPeers[group]); + } + } + else + { + foreach (DataGridItemAutomationPeer itemPeer in _itemPeers.Values) + { + peers.Add(itemPeer); + } + } + + return peers; + } + + internal DataGridGroupItemAutomationPeer GetOrCreateGroupItemPeer(object group) + { + DataGridGroupItemAutomationPeer peer = null; + + if (group != null) + { + if (_groupItemPeers.ContainsKey(group)) + { + peer = _groupItemPeers[group]; + } + else + { + peer = new DataGridGroupItemAutomationPeer(group as ICollectionViewGroup, this.OwningDataGrid); + _groupItemPeers.Add(group, peer); + } + + DataGridRowGroupHeaderAutomationPeer rghPeer = peer.OwningRowGroupHeaderPeer; + if (rghPeer != null) + { + rghPeer.EventsSource = peer; + } + } + + return peer; + } + + internal DataGridItemAutomationPeer GetOrCreateItemPeer(object item) + { + DataGridItemAutomationPeer peer = null; + + if (item != null) + { + if (_itemPeers.ContainsKey(item)) + { + peer = _itemPeers[item]; + } + else + { + peer = new DataGridItemAutomationPeer(item, this.OwningDataGrid); + _itemPeers.Add(item, peer); + } + + DataGridRowAutomationPeer rowPeer = peer.OwningRowPeer; + if (rowPeer != null) + { + rowPeer.EventsSource = peer; + } + } + + return peer; + } + + internal void PopulateGroupItemPeers() + { + Dictionary oldChildren = new Dictionary(_groupItemPeers); + _groupItemPeers.Clear(); + + if (this.OwningDataGrid.DataConnection.CollectionView != null && +#if FEATURE_ICOLLECTIONVIEW_GROUP + this.OwningDataGrid.DataConnection.CollectionView.CanGroup && +#endif + this.OwningDataGrid.DataConnection.CollectionView.CollectionGroups != null && + this.OwningDataGrid.DataConnection.CollectionView.CollectionGroups.Count > 0) + { + List groups = new List(this.OwningDataGrid.DataConnection.CollectionView.CollectionGroups); + while (groups.Count > 0) + { + ICollectionViewGroup cvGroup = groups[0] as ICollectionViewGroup; + groups.RemoveAt(0); + if (cvGroup != null) + { + // Add the group's peer to the collection + DataGridGroupItemAutomationPeer peer = null; + + if (oldChildren.ContainsKey(cvGroup)) + { + peer = oldChildren[cvGroup] as DataGridGroupItemAutomationPeer; + } + else + { + peer = new DataGridGroupItemAutomationPeer(cvGroup, this.OwningDataGrid); + } + + if (peer != null) + { + DataGridRowGroupHeaderAutomationPeer rghPeer = peer.OwningRowGroupHeaderPeer; + if (rghPeer != null) + { + rghPeer.EventsSource = peer; + } + } + + // This guards against the addition of duplicate items + if (!_groupItemPeers.ContainsKey(cvGroup)) + { + _groupItemPeers.Add(cvGroup, peer); + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + // Look for any sub groups + if (!cvGroup.IsBottomLevel) + { + int position = 0; + foreach (object subGroup in cvGroup.Items) + { + groups.Insert(position, subGroup); + position++; + } + } +#endif + } + } + } + } + + internal void PopulateItemPeers() + { + Dictionary oldChildren = new Dictionary(_itemPeers); + _itemPeers.Clear(); + + if (this.OwningDataGrid.ItemsSource != null) + { + foreach (object item in this.OwningDataGrid.ItemsSource) + { + if (item != null) + { + DataGridItemAutomationPeer peer; + if (oldChildren.ContainsKey(item)) + { + peer = oldChildren[item] as DataGridItemAutomationPeer; + } + else + { + peer = new DataGridItemAutomationPeer(item, this.OwningDataGrid); + } + + if (peer != null) + { + DataGridRowAutomationPeer rowPeer = peer.OwningRowPeer; + if (rowPeer != null) + { + rowPeer.EventsSource = peer; + } + } + + // This guards against the addition of duplicate items + if (!_itemPeers.ContainsKey(item)) + { + _itemPeers.Add(item, peer); + } + } + } + } + } + + internal void RaiseAutomationCellSelectedEvent(int slot, int column) + { + AutomationPeer cellPeer = GetCellPeer(slot, column); + if (cellPeer != null) + { +#if DEBUG_AUTOMATION + Debug.WriteLine(cellPeer.ToString() + ".RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected)"); +#endif + cellPeer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected); + } + } + + internal void RaiseAutomationFocusChangedEvent(int slot, int column) + { + if (slot >= 0 && slot < this.OwningDataGrid.SlotCount && + column >= 0 && column < this.OwningDataGrid.ColumnsItemsInternal.Count && + this.OwningDataGrid.IsSlotVisible(slot)) + { + if (this.OwningDataGrid.RowGroupHeadersTable.Contains(slot)) + { + DataGridRowGroupHeader header = this.OwningDataGrid.DisplayData.GetDisplayedElement(slot) as DataGridRowGroupHeader; + if (header != null) + { + AutomationPeer headerPeer = CreatePeerForElement(header); + if (headerPeer != null) + { +#if DEBUG_AUTOMATION + Debug.WriteLine(headerPeer.ToString() + ".RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged)"); +#endif + headerPeer.RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged); + } + } + } + else + { + AutomationPeer cellPeer = GetCellPeer(slot, column); + if (cellPeer != null) + { +#if DEBUG_AUTOMATION + Debug.WriteLine(cellPeer.ToString() + ".RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged)"); +#endif + cellPeer.RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged); + } + } + } + } + + internal void RaiseAutomationInvokeEvents(DataGridEditingUnit editingUnit, DataGridColumn column, DataGridRow row) + { + switch (editingUnit) + { + case DataGridEditingUnit.Cell: + { + DataGridCell cell = row.Cells[column.Index]; + AutomationPeer peer = FromElement(cell); + if (peer != null) + { + peer.InvalidatePeer(); + } + else + { + peer = CreatePeerForElement(cell); + } + + if (peer != null) + { +#if DEBUG_AUTOMATION + Debug.WriteLine(peer.ToString() + ".RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked); + } + + break; + } + + case DataGridEditingUnit.Row: + { + DataGridItemAutomationPeer peer = GetOrCreateItemPeer(row.DataContext); +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridItemAutomationPeer.RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.InvokePatternOnInvoked); + break; + } + } + } + + internal void RaiseAutomationScrollEvents() + { + IScrollProvider isp = (IScrollProvider)this; + + bool newHorizontallyScrollable = isp.HorizontallyScrollable; + double newHorizontalViewSize = isp.HorizontalViewSize; + double newHorizontalScrollPercent = isp.HorizontalScrollPercent; + + bool newVerticallyScrollable = isp.VerticallyScrollable; + double newVerticalViewSize = isp.VerticalViewSize; + double newVerticalScrollPercent = isp.VerticalScrollPercent; + + if (_oldHorizontallyScrollable != newHorizontallyScrollable) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.HorizontallyScrollableProperty, + _oldHorizontallyScrollable, + newHorizontallyScrollable); + _oldHorizontallyScrollable = newHorizontallyScrollable; + } + + if (_oldHorizontalViewSize != newHorizontalViewSize) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.HorizontalViewSizeProperty, + _oldHorizontalViewSize, + newHorizontalViewSize); + _oldHorizontalViewSize = newHorizontalViewSize; + } + + if (_oldHorizontalScrollPercent != newHorizontalScrollPercent) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.HorizontalScrollPercentProperty, + _oldHorizontalScrollPercent, + newHorizontalScrollPercent); + _oldHorizontalScrollPercent = newHorizontalScrollPercent; + } + + if (_oldVerticallyScrollable != newVerticallyScrollable) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.VerticallyScrollableProperty, + _oldVerticallyScrollable, + newVerticallyScrollable); + _oldVerticallyScrollable = newVerticallyScrollable; + } + + if (_oldVerticalViewSize != newVerticalViewSize) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.VerticalViewSizeProperty, + _oldVerticalViewSize, + newVerticalViewSize); + _oldVerticalViewSize = newVerticalViewSize; + } + + if (_oldVerticalScrollPercent != newVerticalScrollPercent) + { + RaisePropertyChangedEvent( + ScrollPatternIdentifiers.VerticalScrollPercentProperty, + _oldVerticalScrollPercent, + newVerticalScrollPercent); + _oldVerticalScrollPercent = newVerticalScrollPercent; + } + } + + internal void RaiseAutomationSelectionEvents(SelectionChangedEventArgs e) + { + // If the result of an AddToSelection or RemoveFromSelection is a single selected item, + // then all we raise is the ElementSelectedEvent for single item + if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected) && + this.OwningDataGrid.SelectedItems.Count == 1) + { + if (this.OwningDataGrid.SelectedItem != null && _itemPeers.ContainsKey(this.OwningDataGrid.SelectedItem)) + { + DataGridItemAutomationPeer peer = _itemPeers[this.OwningDataGrid.SelectedItem]; +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridItemAutomationPeer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected); + } + } + else + { + int i; + + if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementAddedToSelection)) + { + for (i = 0; i < e.AddedItems.Count; i++) + { + if (e.AddedItems[i] != null && _itemPeers.ContainsKey(e.AddedItems[i])) + { + DataGridItemAutomationPeer peer = _itemPeers[e.AddedItems[i]]; +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridItemAutomationPeer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementAddedToSelection)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementAddedToSelection); + } + } + } + + if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection)) + { + for (i = 0; i < e.RemovedItems.Count; i++) + { + if (e.RemovedItems[i] != null && _itemPeers.ContainsKey(e.RemovedItems[i])) + { + DataGridItemAutomationPeer peer = _itemPeers[e.RemovedItems[i]]; +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridItemAutomationPeer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection)"); +#endif + peer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection); + } + } + } + } + } + + internal void UpdateRowGroupHeaderPeerEventsSource(DataGridRowGroupHeader header) + { + object group = header.RowGroupInfo.CollectionViewGroup; + DataGridRowGroupHeaderAutomationPeer peer = DataGridRowGroupHeaderAutomationPeer.FromElement(header) as DataGridRowGroupHeaderAutomationPeer; + if (peer != null && group != null && _groupItemPeers.ContainsKey(group)) + { + peer.EventsSource = _groupItemPeers[group]; + } + } + + internal void UpdateRowPeerEventsSource(DataGridRow row) + { + DataGridRowAutomationPeer peer = FromElement(row) as DataGridRowAutomationPeer; + if (peer != null && row.DataContext != null && _itemPeers.ContainsKey(row.DataContext)) + { + peer.EventsSource = _itemPeers[row.DataContext]; + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridCellAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridCellAutomationPeer.cs new file mode 100644 index 0000000..3dca263 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridCellAutomationPeer.cs @@ -0,0 +1,403 @@ +// 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.Generic; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridCell + /// + public class DataGridCellAutomationPeer : FrameworkElementAutomationPeer, + IGridItemProvider, IInvokeProvider, IScrollItemProvider, ISelectionItemProvider, ITableItemProvider + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridCell + public DataGridCellAutomationPeer(DataGridCell owner) + : base(owner) + { + } + + private IRawElementProviderSimple ContainingGrid + { + get + { + AutomationPeer peer = CreatePeerForElement(this.OwningGrid); + if (peer != null) + { + return ProviderFromPeer(peer); + } + + return null; + } + } + + private DataGridCell OwningCell + { + get + { + return Owner as DataGridCell; + } + } + + private DataGridColumn OwningColumn + { + get + { + return this.OwningCell.OwningColumn; + } + } + + private DataGrid OwningGrid + { + get + { + return this.OwningCell.OwningGrid; + } + } + + private DataGridRow OwningRow + { + get + { + return this.OwningCell.OwningRow; + } + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + if (this.OwningColumn != null) + { + if (this.OwningColumn is DataGridCheckBoxColumn) + { + return AutomationControlType.CheckBox; + } + + if (this.OwningColumn is DataGridTextColumn) + { + return AutomationControlType.Text; + } + + if (this.OwningColumn is DataGridComboBoxColumn) + { + return AutomationControlType.ComboBox; + } + } + + return AutomationControlType.Custom; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridCellAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets the name of the element. + /// + /// The string that contains the name. + protected override string GetNameCore() + { + TextBlock textBlock = this.OwningCell.Content as TextBlock; + if (textBlock != null) + { + return textBlock.Text; + } + + TextBox textBox = this.OwningCell.Content as TextBox; + if (textBox != null) + { + return textBox.Text; + } + + if (this.OwningColumn != null && this.OwningRow != null) + { + object cellContent = null; + DataGridBoundColumn boundColumn = this.OwningColumn as DataGridBoundColumn; + if (boundColumn != null && boundColumn.Binding != null) + { + cellContent = boundColumn.GetCellValue(this.OwningRow.DataContext, boundColumn.Binding); + } + + if (cellContent == null && this.OwningColumn.ClipboardContentBinding != null) + { + cellContent = this.OwningColumn.GetCellValue(this.OwningRow.DataContext, this.OwningColumn.ClipboardContentBinding); + } + + if (cellContent != null) + { + string cellName = cellContent.ToString(); + if (!string.IsNullOrEmpty(cellName)) + { + return cellName; + } + } + } + + return base.GetNameCore(); + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + if (this.OwningGrid != null) + { + switch (patternInterface) + { + case PatternInterface.Invoke: + { + if (!this.OwningGrid.IsReadOnly && + this.OwningColumn != null && + !this.OwningColumn.IsReadOnly) + { + return this; + } + + break; + } + + case PatternInterface.ScrollItem: + { + if (this.OwningGrid.HorizontalScrollBar != null && + this.OwningGrid.HorizontalScrollBar.Maximum > 0) + { + return this; + } + + break; + } + + case PatternInterface.GridItem: + case PatternInterface.SelectionItem: + case PatternInterface.TableItem: + return this; + } + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Gets a value that indicates whether the element can accept keyboard focus. + /// + /// true if the element can accept keyboard focus; otherwise, false + protected override bool IsKeyboardFocusableCore() + { + return true; + } + + int IGridItemProvider.Column + { + get + { + int column = this.OwningCell.ColumnIndex; + if (column >= 0 && this.OwningGrid != null && this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + column--; + } + + return column; + } + } + + int IGridItemProvider.ColumnSpan + { + get + { + return 1; + } + } + + IRawElementProviderSimple IGridItemProvider.ContainingGrid + { + get + { + return this.ContainingGrid; + } + } + + int IGridItemProvider.Row + { + get + { + return this.OwningCell.RowIndex; + } + } + + int IGridItemProvider.RowSpan + { + get + { + return 1; + } + } + + void IInvokeProvider.Invoke() + { + EnsureEnabled(); + + if (this.OwningGrid != null) + { + if (this.OwningGrid.WaitForLostFocus(() => { ((IInvokeProvider)this).Invoke(); })) + { + return; + } + + if (this.OwningGrid.EditingRow == this.OwningRow && this.OwningGrid.EditingColumnIndex == this.OwningCell.ColumnIndex) + { + this.OwningGrid.CommitEdit(DataGridEditingUnit.Cell, true /*exitEditingMode*/); + } + else if (this.OwningGrid.UpdateSelectionAndCurrency(this.OwningCell.ColumnIndex, this.OwningRow.Slot, DataGridSelectionAction.SelectCurrent, true)) + { + this.OwningGrid.BeginEdit(); + } + } + } + + void IScrollItemProvider.ScrollIntoView() + { + if (this.OwningGrid != null) + { + this.OwningGrid.ScrollIntoView(this.OwningCell.DataContext, this.OwningColumn); + } + else + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + bool ISelectionItemProvider.IsSelected + { + get + { + if (this.OwningGrid != null && this.OwningRow != null) + { + return this.OwningRow.IsSelected; + } + + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + IRawElementProviderSimple ISelectionItemProvider.SelectionContainer + { + get + { + AutomationPeer peer = CreatePeerForElement(this.OwningRow); + if (peer != null) + { + return ProviderFromPeer(peer); + } + + return null; + } + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + if (this.OwningCell.OwningGrid == null || + this.OwningCell.OwningGrid.CurrentSlot != this.OwningCell.RowIndex || + this.OwningCell.OwningGrid.CurrentColumnIndex != this.OwningCell.ColumnIndex) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + if (this.OwningCell.OwningGrid == null || + (this.OwningCell.OwningGrid.CurrentSlot == this.OwningCell.RowIndex && + this.OwningCell.OwningGrid.CurrentColumnIndex == this.OwningCell.ColumnIndex)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + void ISelectionItemProvider.Select() + { + EnsureEnabled(); + + if (this.OwningGrid != null) + { + if (this.OwningGrid.WaitForLostFocus(() => { ((ISelectionItemProvider)this).Select(); })) + { + return; + } + + this.OwningGrid.UpdateSelectionAndCurrency(this.OwningCell.ColumnIndex, this.OwningRow.Slot, DataGridSelectionAction.SelectCurrent, false); + } + } + + IRawElementProviderSimple[] ITableItemProvider.GetColumnHeaderItems() + { + if (this.OwningGrid != null && + this.OwningGrid.AreColumnHeadersVisible && + this.OwningColumn.HeaderCell != null) + { + AutomationPeer peer = CreatePeerForElement(this.OwningColumn.HeaderCell); + if (peer != null) + { + List providers = new List(1); + providers.Add(ProviderFromPeer(peer)); + return providers.ToArray(); + } + } + + return null; + } + + IRawElementProviderSimple[] ITableItemProvider.GetRowHeaderItems() + { + if (this.OwningGrid != null && + this.OwningGrid.AreRowHeadersVisible && + this.OwningRow.HeaderCell != null) + { + AutomationPeer peer = CreatePeerForElement(this.OwningRow.HeaderCell); + if (peer != null) + { + List providers = new List(1); + providers.Add(ProviderFromPeer(peer)); + return providers.ToArray(); + } + } + + return null; + } + + private void EnsureEnabled() + { + if (!IsEnabled()) + { + throw new ElementNotEnabledException(); + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeaderAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeaderAutomationPeer.cs new file mode 100644 index 0000000..1ae16f6 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeaderAutomationPeer.cs @@ -0,0 +1,200 @@ +// 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.ComponentModel; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridColumnHeader + /// + public class DataGridColumnHeaderAutomationPeer : FrameworkElementAutomationPeer, + IInvokeProvider, IScrollItemProvider, ITransformProvider + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridColumnHeader + public DataGridColumnHeaderAutomationPeer(DataGridColumnHeader owner) + : base(owner) + { + } + + private DataGridColumnHeader OwningHeader + { + get + { + return Owner as DataGridColumnHeader; + } + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.HeaderItem; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridColumnHeaderAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets the string that describes the functionality of the control that is associated with the automation peer. + /// + /// The string that contains the help text. + protected override string GetHelpTextCore() + { + if (this.OwningHeader.OwningColumn != null && this.OwningHeader.OwningColumn.SortDirection.HasValue) + { + if (this.OwningHeader.OwningColumn.SortDirection.Value == DataGridSortDirection.Ascending) + { + return "Ascending"; + } + + return "Descending"; + } + + return base.GetHelpTextCore(); + } + + /// + /// Gets the name of the element. + /// + /// The string that contains the name. + protected override string GetNameCore() + { + string header = this.OwningHeader.Content as string; + if (header != null) + { + return header; + } + + return base.GetNameCore(); + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + if (this.OwningHeader.OwningGrid != null) + { + switch (patternInterface) + { + case PatternInterface.Invoke: + // this.OwningHeader.OwningGrid.DataConnection.AllowSort property is ignored because of the DataGrid.Sorting custom sorting capability. + if (this.OwningHeader.OwningGrid.CanUserSortColumns && + this.OwningHeader.OwningColumn.CanUserSort) + { + return this; + } + + break; + + case PatternInterface.ScrollItem: + if (this.OwningHeader.OwningGrid.HorizontalScrollBar != null && + this.OwningHeader.OwningGrid.HorizontalScrollBar.Maximum > 0) + { + return this; + } + + break; + + case PatternInterface.Transform: + if (this.OwningHeader.OwningColumn != null && + this.OwningHeader.OwningColumn.ActualCanUserResize) + { + return this; + } + + break; + } + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Gets a value that specifies whether the element is a content element. + /// + /// True if the element is a content element; otherwise false + protected override bool IsContentElementCore() + { + return false; + } + + void IInvokeProvider.Invoke() + { + this.OwningHeader.InvokeProcessSort(); + } + + void IScrollItemProvider.ScrollIntoView() + { + this.OwningHeader.OwningGrid.ScrollIntoView(null, this.OwningHeader.OwningColumn); + } + + bool ITransformProvider.CanMove + { + get + { + return false; + } + } + + bool ITransformProvider.CanResize + { + get + { + return this.OwningHeader.OwningColumn != null && this.OwningHeader.OwningColumn.ActualCanUserResize; + } + } + + bool ITransformProvider.CanRotate + { + get + { + return false; + } + } + + void ITransformProvider.Move(double x, double y) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + void ITransformProvider.Resize(double width, double height) + { + if (this.OwningHeader.OwningColumn != null && + this.OwningHeader.OwningColumn.ActualCanUserResize) + { + this.OwningHeader.OwningColumn.Width = new DataGridLength(width); + } + } + + void ITransformProvider.Rotate(double degrees) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeadersPresenterAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeadersPresenterAutomationPeer.cs new file mode 100644 index 0000000..329b16d --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridColumnHeadersPresenterAutomationPeer.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridColumnHeadersPresenter + /// + public class DataGridColumnHeadersPresenterAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridColumnHeadersPresenter + public DataGridColumnHeadersPresenterAutomationPeer(DataGridColumnHeadersPresenter owner) + : base(owner) + { + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Header; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridColumnHeadersPresenterAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets a value that specifies whether the element is a content element. + /// + /// True if the element is a content element; otherwise false + protected override bool IsContentElementCore() + { + return false; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridDetailsPresenterAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridDetailsPresenterAutomationPeer.cs new file mode 100644 index 0000000..1c98a20 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridDetailsPresenterAutomationPeer.cs @@ -0,0 +1,67 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridDetailsPresenter + /// + public class DataGridDetailsPresenterAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridDetailsPresenter + public DataGridDetailsPresenterAutomationPeer(DataGridDetailsPresenter owner) + : base(owner) + { + } + + /// + /// Gets the control type for the DataGridDetailsPresenter element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Custom; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridDetailsPresenterAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets or sets a value indicating whether the DataGridDetailsPresenter associated with this UIElementAutomationPeer + /// is understood by the end user as interactive. + /// + /// True if the DataGridDetailsPresenter associated with this UIElementAutomationPeer + /// is understood by the end user as interactive. + protected override bool IsControlElementCore() + { + return true; + } + + /// + /// Gets a value that specifies whether the element is a content element. + /// + /// True if the element is a content element; otherwise false + protected override bool IsContentElementCore() + { + return false; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridGroupItemAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridGroupItemAutomationPeer.cs new file mode 100644 index 0000000..7113caf --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridGroupItemAutomationPeer.cs @@ -0,0 +1,575 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for a group of items in a DataGrid + /// + public class DataGridGroupItemAutomationPeer : FrameworkElementAutomationPeer, + IExpandCollapseProvider, IGridProvider, IScrollItemProvider, ISelectionProvider + { + private ICollectionViewGroup _group; + private AutomationPeer _dataGridAutomationPeer; + + /// + /// Initializes a new instance of the class. + /// + public DataGridGroupItemAutomationPeer(ICollectionViewGroup group, DataGrid dataGrid) + : base(dataGrid) + { + if (group == null) + { + throw new ArgumentNullException("group"); + } + + if (dataGrid == null) + { + throw new ArgumentNullException("dataGrid"); + } + + _group = group; + _dataGridAutomationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(dataGrid); + } + + /// + /// Gets the owning DataGrid + /// + private DataGrid OwningDataGrid + { + get + { + DataGridAutomationPeer gridPeer = _dataGridAutomationPeer as DataGridAutomationPeer; + return gridPeer.Owner as DataGrid; + } + } + + /// + /// Gets the owning DataGrid's Automation Peer + /// + private DataGridAutomationPeer OwningDataGridPeer + { + get + { + return _dataGridAutomationPeer as DataGridAutomationPeer; + } + } + + /// + /// Gets the owning DataGridRowGroupHeader + /// + private DataGridRowGroupHeader OwningRowGroupHeader + { + get + { + if (this.OwningDataGrid != null) + { + DataGridRowGroupInfo groupInfo = this.OwningDataGrid.RowGroupInfoFromCollectionViewGroup(_group); + if (groupInfo != null && this.OwningDataGrid.IsSlotVisible(groupInfo.Slot)) + { + return this.OwningDataGrid.DisplayData.GetDisplayedElement(groupInfo.Slot) as DataGridRowGroupHeader; + } + } + + return null; + } + } + + /// + /// Gets the owning DataGridRowGroupHeader's Automation Peer + /// + internal DataGridRowGroupHeaderAutomationPeer OwningRowGroupHeaderPeer + { + get + { + DataGridRowGroupHeaderAutomationPeer rowGroupHeaderPeer = null; + DataGridRowGroupHeader rowGroupHeader = this.OwningRowGroupHeader; + if (rowGroupHeader != null) + { + rowGroupHeaderPeer = FrameworkElementAutomationPeer.FromElement(rowGroupHeader) as DataGridRowGroupHeaderAutomationPeer; + if (rowGroupHeaderPeer == null) + { + rowGroupHeaderPeer = FrameworkElementAutomationPeer.CreatePeerForElement(rowGroupHeader) as DataGridRowGroupHeaderAutomationPeer; + } + } + + return rowGroupHeaderPeer; + } + } + + /// + /// Returns the accelerator key for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The accelerator key for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override string GetAcceleratorKeyCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetAcceleratorKey() : string.Empty; + } + + /// + /// Returns the access key for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The access key for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override string GetAccessKeyCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetAccessKey() : string.Empty; + } + + /// + /// Returns the control type for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The control type for the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Group; + } + + /// + /// Returns the string that uniquely identifies the FrameworkElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The string that uniquely identifies the FrameworkElement that is associated with this DataGridGroupItemAutomationPeer. + protected override string GetAutomationIdCore() + { + // The AutomationId should be unset for dynamic content. + return string.Empty; + } + + /// + /// Returns the Rect that represents the bounding rectangle of the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The Rect that represents the bounding rectangle of the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override Rect GetBoundingRectangleCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.GetBoundingRectangle() : default(Rect); + } + + /// + /// Returns the collection of elements that are represented in the UI Automation tree as immediate + /// child elements of the automation peer. + /// + /// The children elements. + protected override IList GetChildrenCore() + { + List children = new List(); + if (this.OwningRowGroupHeaderPeer != null) + { + this.OwningRowGroupHeaderPeer.InvalidatePeer(); + children.AddRange(this.OwningRowGroupHeaderPeer.GetChildren()); + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (_group.IsBottomLevel) + { +#endif +#pragma warning disable SA1137 // Elements should have the same indentation + foreach (object item in _group.GroupItems /*Items*/) + { + children.Add(this.OwningDataGridPeer.GetOrCreateItemPeer(item)); + } +#pragma warning restore SA1137 // Elements should have the same indentation +#if FEATURE_ICOLLECTIONVIEW_GROUP + } + else + { + foreach (object group in _group.Items) + { + children.Add(this.OwningDataGridPeer.GetOrCreateGroupItemPeer(group)); + } + } +#endif + return children; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.GetClassName() : string.Empty; +#if DEBUG_AUTOMATION + Debug.WriteLine("DataGridGroupItemAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Returns a Point that represents the clickable space that is on the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// A Point that represents the clickable space that is on the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override Point GetClickablePointCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.GetClickablePoint() : new Point(double.NaN, double.NaN); + } + + /// + /// Returns the string that describes the functionality of the control that is associated with the automation peer. + /// + /// The string that contains the help text. + protected override string GetHelpTextCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.GetHelpText() : string.Empty; + } + + /// + /// Returns a string that communicates the visual status of the UIElement that is associated with this DataGridGroupItemAutomationPeer. + /// + /// A string that communicates the visual status of the UIElement that is associated with this DataGridGroupItemAutomationPeer. + protected override string GetItemStatusCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.GetItemStatus() : string.Empty; + } + + /// + /// Returns a human-readable string that contains the item type that the UIElement for this DataGridGroupItemAutomationPeer represents. + /// + /// A human-readable string that contains the item type that the UIElement for this DataGridGroupItemAutomationPeer represents. + protected override string GetItemTypeCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetItemType() : string.Empty; + } + + /// + /// Returns the AutomationPeer for the element that is targeted to the UIElement for this DataGridGroupItemAutomationPeer. + /// + /// The AutomationPeer for the element that is targeted to the UIElement for this DataGridGroupItemAutomationPeer. + protected override AutomationPeer GetLabeledByCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetLabeledBy() : null; + } + + /// + /// Returns a localized human readable string for this control type. + /// + /// A localized human readable string for this control type. + protected override string GetLocalizedControlTypeCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetLocalizedControlType() : string.Empty; + } + + /// + /// Returns the string that describes the functionality of the control that is associated with this DataGridGroupItemAutomationPeer. + /// + /// The string that contains the help text. + protected override string GetNameCore() + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + if (_group.Name != null) + { + string name = _group.Name.ToString(); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + } +#endif + return base.GetNameCore(); + } + + /// + /// Returns a value indicating whether the element associated with this DataGridGroupItemAutomationPeer is laid out in a specific direction. + /// + /// A value indicating whether the element associated with this DataGridGroupItemAutomationPeer is laid out in a specific direction. + protected override AutomationOrientation GetOrientationCore() + { + return (this.OwningRowGroupHeaderPeer != null) ? this.OwningRowGroupHeaderPeer.GetOrientation() : AutomationOrientation.None; + } + + /// + /// Gets the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + switch (patternInterface) + { + case PatternInterface.ExpandCollapse: + case PatternInterface.Grid: + case PatternInterface.Selection: + case PatternInterface.Table: + return this; + case PatternInterface.ScrollItem: + { + if (this.OwningDataGrid.VerticalScrollBar != null && + this.OwningDataGrid.VerticalScrollBar.Maximum > 0) + { + return this; + } + + break; + } + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Returns a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer can accept keyboard focus. + /// + /// True if the element is focusable by the keyboard; otherwise false. + protected override bool HasKeyboardFocusCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.HasKeyboardFocus() : false; + } + + /// + /// Returns a value indicating whether the element associated with this DataGridGroupItemAutomationPeer is an element that contains data that is presented to the user. + /// + /// True if the element contains data for the user to read; otherwise, false. + protected override bool IsContentElementCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsContentElement() : true; + } + + /// + /// Gets or sets a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer + /// is understood by the end user as interactive. + /// + /// True if the UIElement associated with this DataGridGroupItemAutomationPeer + /// is understood by the end user as interactive. + protected override bool IsControlElementCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsControlElement() : true; + } + + /// + /// Gets a value indicating whether this DataGridGroupItemAutomationPeer can receive and send events to the associated element. + /// + /// True if this DataGridGroupItemAutomationPeer can receive and send events; otherwise, false. + protected override bool IsEnabledCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsEnabled() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer can accept keyboard focus. + /// + /// True if the UIElement associated with this DataGridGroupItemAutomationPeer can accept keyboard focus. + protected override bool IsKeyboardFocusableCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsKeyboardFocusable() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer is off the screen. + /// + /// True if the element is not on the screen; otherwise, false. + protected override bool IsOffscreenCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsOffscreen() : true; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer contains protected content. + /// + /// True if the UIElement contains protected content. + protected override bool IsPasswordCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsPassword() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridGroupItemAutomationPeer is required to be completed on a form. + /// + /// True if the UIElement is required to be completed on a form. + protected override bool IsRequiredForFormCore() + { + return this.OwningRowGroupHeaderPeer != null ? this.OwningRowGroupHeaderPeer.IsRequiredForForm() : false; + } + + /// + /// Sets the keyboard input focus on the UIElement associated with this DataGridGroupItemAutomationPeer. + /// + protected override void SetFocusCore() + { + if (this.OwningRowGroupHeaderPeer != null) + { + this.OwningRowGroupHeaderPeer.SetFocus(); + } + } + + void IExpandCollapseProvider.Collapse() + { + EnsureEnabled(); + + if (this.OwningDataGrid != null) + { + this.OwningDataGrid.CollapseRowGroup(_group, false /*collapseAllSubgroups*/); + } + } + + void IExpandCollapseProvider.Expand() + { + EnsureEnabled(); + + if (this.OwningDataGrid != null) + { + this.OwningDataGrid.ExpandRowGroup(_group, false /*expandAllSubgroups*/); + } + } + + ExpandCollapseState IExpandCollapseProvider.ExpandCollapseState + { + get + { + if (this.OwningDataGrid != null) + { + DataGridRowGroupInfo groupInfo = this.OwningDataGrid.RowGroupInfoFromCollectionViewGroup(_group); + if (groupInfo != null && groupInfo.Visibility == Visibility.Visible) + { + return ExpandCollapseState.Expanded; + } + } + + return ExpandCollapseState.Collapsed; + } + } + + int IGridProvider.ColumnCount + { + get + { + if (this.OwningDataGrid != null) + { + return this.OwningDataGrid.Columns.Count; + } + + return 0; + } + } + + IRawElementProviderSimple IGridProvider.GetItem(int row, int column) + { + EnsureEnabled(); + + if (this.OwningDataGrid != null && + this.OwningDataGrid.DataConnection != null && + row >= 0 && row < _group.GroupItems.Count /*ItemCount*/ && + column >= 0 && column < this.OwningDataGrid.Columns.Count) + { + DataGridRowGroupInfo groupInfo = this.OwningDataGrid.RowGroupInfoFromCollectionViewGroup(_group); + if (groupInfo != null) + { + // Adjust the row index to be relative to the DataGrid instead of the group + row = groupInfo.Slot - this.OwningDataGrid.RowGroupHeadersTable.GetIndexCount(0, groupInfo.Slot) + row + 1; + Debug.Assert(row >= 0, "Expected positive row."); + Debug.Assert(row < this.OwningDataGrid.DataConnection.Count, "Expected row smaller than this.OwningDataGrid.DataConnection.Count."); + int slot = this.OwningDataGrid.SlotFromRowIndex(row); + + if (!this.OwningDataGrid.IsSlotVisible(slot)) + { + object item = this.OwningDataGrid.DataConnection.GetDataItem(row); + this.OwningDataGrid.ScrollIntoView(item, this.OwningDataGrid.Columns[column]); + } + + Debug.Assert(this.OwningDataGrid.IsSlotVisible(slot), "Expected OwningDataGrid.IsSlotVisible(slot) is true."); + + DataGridRow dgr = this.OwningDataGrid.DisplayData.GetDisplayedElement(slot) as DataGridRow; + + // the first cell is always the indentation filler cell if grouping is enabled, so skip it + Debug.Assert(column + 1 < dgr.Cells.Count, "Expected column + 1 smaller than dgr.Cells.Count."); + DataGridCell cell = dgr.Cells[column + 1]; + AutomationPeer peer = CreatePeerForElement(cell); + if (peer != null) + { + return ProviderFromPeer(peer); + } + } + } + + return null; + } + + int IGridProvider.RowCount + { + get + { + return _group.GroupItems.Count /*ItemCount*/; + } + } + + void IScrollItemProvider.ScrollIntoView() + { + EnsureEnabled(); + + if (this.OwningDataGrid != null) + { + DataGridRowGroupInfo groupInfo = this.OwningDataGrid.RowGroupInfoFromCollectionViewGroup(_group); + if (groupInfo != null) + { + this.OwningDataGrid.ScrollIntoView(groupInfo.CollectionViewGroup, null); + } + } + } + + IRawElementProviderSimple[] ISelectionProvider.GetSelection() + { + EnsureEnabled(); + + if (this.OwningDataGrid != null && + this.OwningDataGridPeer != null && + this.OwningDataGrid.SelectedItems != null && + _group.GroupItems.Count /*ItemCount*/ > 0) + { + DataGridRowGroupInfo groupInfo = this.OwningDataGrid.RowGroupInfoFromCollectionViewGroup(_group); + if (groupInfo != null) + { + // See which of the selected items are contained within this group + List selectedProviders = new List(); + int startRowIndex = groupInfo.Slot - this.OwningDataGrid.RowGroupHeadersTable.GetIndexCount(0, groupInfo.Slot) + 1; + foreach (object item in this.OwningDataGrid.GetSelectionInclusive(startRowIndex, startRowIndex + _group.GroupItems.Count /*ItemCount*/ - 1)) + { + DataGridItemAutomationPeer peer = this.OwningDataGridPeer.GetOrCreateItemPeer(item); + if (peer != null) + { + selectedProviders.Add(ProviderFromPeer(peer)); + } + } + + return selectedProviders.ToArray(); + } + } + + return null; + } + + bool ISelectionProvider.CanSelectMultiple + { + get + { + return this.OwningDataGrid != null && this.OwningDataGrid.SelectionMode == DataGridSelectionMode.Extended; + } + } + + bool ISelectionProvider.IsSelectionRequired + { + get + { + return false; + } + } + + private void EnsureEnabled() + { + if (!_dataGridAutomationPeer.IsEnabled()) + { + throw new ElementNotEnabledException(); + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridItemAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridItemAutomationPeer.cs new file mode 100644 index 0000000..3254203 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridItemAutomationPeer.cs @@ -0,0 +1,541 @@ +// 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; +using System.Collections.Generic; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.Foundation; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Automation.Provider; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for an item in a DataGrid + /// + public class DataGridItemAutomationPeer : FrameworkElementAutomationPeer, + IInvokeProvider, IScrollItemProvider, ISelectionItemProvider, ISelectionProvider + { + private object _item; + private AutomationPeer _dataGridAutomationPeer; + + /// + /// Initializes a new instance of the class. + /// + public DataGridItemAutomationPeer(object item, DataGrid dataGrid) + : base(dataGrid) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + if (dataGrid == null) + { + throw new ArgumentNullException("dataGrid"); + } + + _item = item; + _dataGridAutomationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(dataGrid); + } + + private DataGrid OwningDataGrid + { + get + { + DataGridAutomationPeer gridPeer = _dataGridAutomationPeer as DataGridAutomationPeer; + return gridPeer.Owner as DataGrid; + } + } + + private DataGridRow OwningRow + { + get + { + int index = this.OwningDataGrid.DataConnection.IndexOf(_item); + int slot = this.OwningDataGrid.SlotFromRowIndex(index); + + if (this.OwningDataGrid.IsSlotVisible(slot)) + { + return this.OwningDataGrid.DisplayData.GetDisplayedElement(slot) as DataGridRow; + } + + return null; + } + } + + internal DataGridRowAutomationPeer OwningRowPeer + { + get + { + DataGridRowAutomationPeer rowPeer = null; + DataGridRow row = this.OwningRow; + if (row != null) + { + rowPeer = FrameworkElementAutomationPeer.CreatePeerForElement(row) as DataGridRowAutomationPeer; + } + + return rowPeer; + } + } + + /// + /// Returns the accelerator key for the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// The accelerator key for the UIElement that is associated with this DataGridItemAutomationPeer. + protected override string GetAcceleratorKeyCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetAcceleratorKey() : string.Empty; + } + + /// + /// Returns the access key for the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// The access key for the UIElement that is associated with this DataGridItemAutomationPeer. + protected override string GetAccessKeyCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetAccessKey() : string.Empty; + } + + /// + /// Returns the control type for the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// The control type for the UIElement that is associated with this DataGridItemAutomationPeer. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.DataItem; + } + + /// + /// Returns the string that uniquely identifies the FrameworkElement that is associated with this DataGridItemAutomationPeer. + /// + /// The string that uniquely identifies the FrameworkElement that is associated with this DataGridItemAutomationPeer. + protected override string GetAutomationIdCore() + { + // The AutomationId should be unset for dynamic content. + return string.Empty; + } + + /// + /// Returns the Rect that represents the bounding rectangle of the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// The Rect that represents the bounding rectangle of the UIElement that is associated with this DataGridItemAutomationPeer. + protected override Rect GetBoundingRectangleCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetBoundingRectangle() : default(Rect); + } + + /// + /// Returns the collection of elements that are represented in the UI Automation tree as immediate + /// child elements of the automation peer. + /// + /// The children elements. + protected override IList GetChildrenCore() + { + if (this.OwningRowPeer != null) + { + this.OwningRowPeer.InvalidatePeer(); + return this.OwningRowPeer.GetChildren(); + } + + return new List(); + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = (this.OwningRowPeer != null) ? this.OwningRowPeer.GetClassName() : string.Empty; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridItemAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Returns a Point that represents the clickable space that is on the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// A Point that represents the clickable space that is on the UIElement that is associated with this DataGridItemAutomationPeer. + protected override Point GetClickablePointCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetClickablePoint() : new Point(double.NaN, double.NaN); + } + + /// + /// Returns the string that describes the functionality of the control that is associated with the automation peer. + /// + /// The string that contains the help text. + protected override string GetHelpTextCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetHelpText() : string.Empty; + } + + /// + /// Returns a string that communicates the visual status of the UIElement that is associated with this DataGridItemAutomationPeer. + /// + /// A string that communicates the visual status of the UIElement that is associated with this DataGridItemAutomationPeer. + protected override string GetItemStatusCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetItemStatus() : string.Empty; + } + + /// + /// Returns a human-readable string that contains the item type that the UIElement for this DataGridItemAutomationPeer represents. + /// + /// A human-readable string that contains the item type that the UIElement for this DataGridItemAutomationPeer represents. + protected override string GetItemTypeCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetItemType() : string.Empty; + } + + /// + /// Returns the AutomationPeer for the element that is targeted to the UIElement for this DataGridItemAutomationPeer. + /// + /// The AutomationPeer for the element that is targeted to the UIElement for this DataGridItemAutomationPeer. + protected override AutomationPeer GetLabeledByCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetLabeledBy() : null; + } + + /// + /// Returns a localized human readable string for this control type. + /// + /// A localized human readable string for this control type. + protected override string GetLocalizedControlTypeCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetLocalizedControlType() : string.Empty; + } + + /// + /// Returns the string that describes the functionality of the control that is associated with this DataGridItemAutomationPeer. + /// + /// The string that contains the help text. + protected override string GetNameCore() + { + if (this.OwningRowPeer != null) + { + string owningRowPeerName = this.OwningRowPeer.GetName(); + if (!string.IsNullOrEmpty(owningRowPeerName)) + { +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridItemAutomationPeer.GetNameCore returns " + owningRowPeerName); +#endif + return owningRowPeerName; + } + } + + string name = UI.Controls.Properties.Resources.DataGridRowAutomationPeer_ItemType; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridItemAutomationPeer.GetNameCore returns " + name); +#endif + return name; + } + + /// + /// Returns a value indicating whether the element associated with this DataGridItemAutomationPeer is laid out in a specific direction. + /// + /// A value indicating whether the element associated with this DataGridItemAutomationPeer is laid out in a specific direction. + protected override AutomationOrientation GetOrientationCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.GetOrientation() : AutomationOrientation.None; + } + + /// + /// Returns the control pattern that is associated with the specified Windows.UI.Xaml.Automation.Peers.PatternInterface. + /// + /// A value from the Windows.UI.Xaml.Automation.Peers.PatternInterface enumeration. + /// The object that supports the specified pattern, or null if unsupported. + protected override object GetPatternCore(PatternInterface patternInterface) + { + switch (patternInterface) + { + case PatternInterface.Invoke: + { + if (!this.OwningDataGrid.IsReadOnly) + { + return this; + } + + break; + } + + case PatternInterface.ScrollItem: + { + if (this.OwningDataGrid.VerticalScrollBar != null && + this.OwningDataGrid.VerticalScrollBar.Maximum > 0) + { + return this; + } + + break; + } + + case PatternInterface.Selection: + case PatternInterface.SelectionItem: + return this; + } + + return base.GetPatternCore(patternInterface); + } + + /// + /// Returns a value indicating whether the UIElement associated with this DataGridItemAutomationPeer can accept keyboard focus. + /// + /// True if the element is focusable by the keyboard; otherwise false. + protected override bool HasKeyboardFocusCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.HasKeyboardFocus() : false; + } + + /// + /// Returns a value indicating whether the element associated with this DataGridItemAutomationPeer is an element that contains data that is presented to the user. + /// + /// True if the element contains data for the user to read; otherwise, false. + protected override bool IsContentElementCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsContentElement() : true; + } + + /// + /// Gets or sets a value indicating whether the UIElement associated with this DataGridItemAutomationPeer + /// is understood by the end user as interactive. + /// + /// True if the UIElement associated with this DataGridItemAutomationPeer + /// is understood by the end user as interactive. + protected override bool IsControlElementCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsControlElement() : true; + } + + /// + /// Gets a value indicating whether this DataGridItemAutomationPeer can receive and send events to the associated element. + /// + /// True if this DataGridItemAutomationPeer can receive and send events; otherwise, false. + protected override bool IsEnabledCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsEnabled() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridItemAutomationPeer can accept keyboard focus. + /// + /// True if the UIElement associated with this DataGridItemAutomationPeer can accept keyboard focus. + protected override bool IsKeyboardFocusableCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsKeyboardFocusable() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridItemAutomationPeer is off the screen. + /// + /// True if the element is not on the screen; otherwise, false. + protected override bool IsOffscreenCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsOffscreen() : true; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridItemAutomationPeer contains protected content. + /// + /// True if the UIElement contains protected content. + protected override bool IsPasswordCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsPassword() : false; + } + + /// + /// Gets a value indicating whether the UIElement associated with this DataGridItemAutomationPeer is required to be completed on a form. + /// + /// True if the UIElement is required to be completed on a form. + protected override bool IsRequiredForFormCore() + { + return this.OwningRowPeer != null ? this.OwningRowPeer.IsRequiredForForm() : false; + } + + /// + /// Sets the keyboard input focus on the UIElement associated with this DataGridItemAutomationPeer. + /// + protected override void SetFocusCore() + { + if (this.OwningRowPeer != null) + { + this.OwningRowPeer.SetFocus(); + } + } + + void IInvokeProvider.Invoke() + { + EnsureEnabled(); + + if (this.OwningRowPeer == null) + { + this.OwningDataGrid.ScrollIntoView(_item, null); + } + + bool success = false; + if (this.OwningRow != null) + { + if (this.OwningDataGrid.WaitForLostFocus(() => { ((IInvokeProvider)this).Invoke(); })) + { + return; + } + + if (this.OwningDataGrid.EditingRow == this.OwningRow) + { + success = this.OwningDataGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/); + } + else if (this.OwningDataGrid.UpdateSelectionAndCurrency(this.OwningDataGrid.CurrentColumnIndex, this.OwningRow.Slot, DataGridSelectionAction.SelectCurrent, false)) + { + success = this.OwningDataGrid.BeginEdit(); + } + } + } + + void IScrollItemProvider.ScrollIntoView() + { + this.OwningDataGrid.ScrollIntoView(_item, null); + } + + bool ISelectionItemProvider.IsSelected + { + get + { + return this.OwningDataGrid.SelectedItems.Contains(_item); + } + } + + IRawElementProviderSimple ISelectionItemProvider.SelectionContainer + { + get + { + return ProviderFromPeer(_dataGridAutomationPeer); + } + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + + if (this.OwningDataGrid.SelectionMode == DataGridSelectionMode.Single && + this.OwningDataGrid.SelectedItems.Count > 0 && + !this.OwningDataGrid.SelectedItems.Contains(_item)) + { + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + int index = this.OwningDataGrid.DataConnection.IndexOf(_item); + if (index != -1) + { + this.OwningDataGrid.SetRowSelection(this.OwningDataGrid.SlotFromRowIndex(index), true, false); + return; + } + + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + + int index = this.OwningDataGrid.DataConnection.IndexOf(_item); + if (index != -1) + { + bool success = true; + if (this.OwningDataGrid.EditingRow != null && this.OwningDataGrid.EditingRow.Index == index) + { + if (this.OwningDataGrid.WaitForLostFocus(() => { ((ISelectionItemProvider)this).RemoveFromSelection(); })) + { + return; + } + + success = this.OwningDataGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/); + } + + if (success) + { + this.OwningDataGrid.SetRowSelection(this.OwningDataGrid.SlotFromRowIndex(index), false, false); + return; + } + + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + void ISelectionItemProvider.Select() + { + EnsureEnabled(); + + int index = this.OwningDataGrid.DataConnection.IndexOf(_item); + if (index != -1) + { + bool success = true; + if (this.OwningDataGrid.EditingRow != null && this.OwningDataGrid.EditingRow.Index != index) + { + if (this.OwningDataGrid.WaitForLostFocus(() => { ((ISelectionItemProvider)this).Select(); })) + { + return; + } + + success = this.OwningDataGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/); + } + + if (success) + { + // Clear all the other selected items and select this one + int slot = this.OwningDataGrid.SlotFromRowIndex(index); + this.OwningDataGrid.UpdateSelectionAndCurrency(this.OwningDataGrid.CurrentColumnIndex, slot, DataGridSelectionAction.SelectCurrent, false); + return; + } + + throw DataGridError.DataGridAutomationPeer.OperationCannotBePerformed(); + } + } + + bool ISelectionProvider.CanSelectMultiple + { + get + { + return false; + } + } + + bool ISelectionProvider.IsSelectionRequired + { + get + { + return false; + } + } + + IRawElementProviderSimple[] ISelectionProvider.GetSelection() + { + if (this.OwningRow != null && + this.OwningDataGrid.IsSlotVisible(this.OwningRow.Slot) && + this.OwningDataGrid.CurrentSlot == this.OwningRow.Slot) + { + DataGridCell cell = this.OwningRow.Cells[this.OwningRow.OwningGrid.CurrentColumnIndex]; + AutomationPeer peer = FrameworkElementAutomationPeer.CreatePeerForElement(cell); + if (peer != null) + { + return new IRawElementProviderSimple[] { ProviderFromPeer(peer) }; + } + } + + return null; + } + + private void EnsureEnabled() + { + if (!_dataGridAutomationPeer.IsEnabled()) + { + throw new ElementNotEnabledException(); + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowAutomationPeer.cs new file mode 100644 index 0000000..488b86c --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowAutomationPeer.cs @@ -0,0 +1,62 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridRow + /// + public class DataGridRowAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridRow + public DataGridRowAutomationPeer(DataGridRow owner) + : base(owner) + { + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.DataItem; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridRowAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Returns a human-readable string that contains the item type that the UIElement for this DataGridRowAutomationPeer represents. + /// + /// A human-readable string that contains the item type that the UIElement for this DataGridRowAutomationPeer represents. + protected override string GetItemTypeCore() + { + string itemType = base.GetItemTypeCore(); + if (!string.IsNullOrEmpty(itemType)) + { + return itemType; + } + + return UI.Controls.Properties.Resources.DataGridRowAutomationPeer_ItemType; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowGroupHeaderAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowGroupHeaderAutomationPeer.cs new file mode 100644 index 0000000..cf44f22 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowGroupHeaderAutomationPeer.cs @@ -0,0 +1,47 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridRowGroupHeader + /// + public class DataGridRowGroupHeaderAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridRowGroupHeader + public DataGridRowGroupHeaderAutomationPeer(DataGridRowGroupHeader owner) + : base(owner) + { + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Group; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridRowGroupHeaderAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowHeaderAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowHeaderAutomationPeer.cs new file mode 100644 index 0000000..b9a1db2 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowHeaderAutomationPeer.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for DataGridRowHeader + /// + public class DataGridRowHeaderAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// DataGridRowHeader + public DataGridRowHeaderAutomationPeer(DataGridRowHeader owner) + : base(owner) + { + } + + private DataGridRowHeader OwningHeader + { + get + { + return (DataGridRowHeader)Owner; + } + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.HeaderItem; + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridRowHeaderAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets the name of the element. + /// + /// The string that contains the name. + protected override string GetNameCore() + { + return (this.OwningHeader.Content as string) ?? base.GetNameCore(); + } + + /// + /// Gets a value that specifies whether the element is a content element. + /// + /// True if the element is a content element; otherwise false + protected override bool IsContentElementCore() + { + return false; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowsPresenterAutomationPeer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowsPresenterAutomationPeer.cs new file mode 100644 index 0000000..7007304 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/Automation/DataGridRowsPresenterAutomationPeer.cs @@ -0,0 +1,93 @@ +// 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.Generic; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml.Automation.Peers; + +namespace Microsoft.Toolkit.Uwp.UI.Automation.Peers +{ + /// + /// AutomationPeer for the class. + /// + public class DataGridRowsPresenterAutomationPeer : FrameworkElementAutomationPeer + { + /// + /// Initializes a new instance of the class. + /// + /// Owning DataGridRowsPresenter + public DataGridRowsPresenterAutomationPeer(DataGridRowsPresenter owner) + : base(owner) + { + } + + private DataGridAutomationPeer GridPeer + { + get + { + if (this.OwningRowsPresenter.OwningGrid != null) + { + return CreatePeerForElement(this.OwningRowsPresenter.OwningGrid) as DataGridAutomationPeer; + } + + return null; + } + } + + private DataGridRowsPresenter OwningRowsPresenter + { + get + { + return Owner as DataGridRowsPresenter; + } + } + + /// + /// Gets the control type for the element that is associated with the UI Automation peer. + /// + /// The control type. + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Custom; + } + + /// + /// Gets the collection of elements that are represented in the UI Automation tree as immediate + /// child elements of the automation peer. + /// + /// The children elements. + protected override IList GetChildrenCore() + { + if (this.OwningRowsPresenter.OwningGrid == null) + { + return new List(); + } + + return this.GridPeer.GetChildPeers(); + } + + /// + /// Called by GetClassName that gets a human readable name that, in addition to AutomationControlType, + /// differentiates the control represented by this AutomationPeer. + /// + /// The string that contains the name. + protected override string GetClassNameCore() + { + string classNameCore = Owner.GetType().Name; +#if DEBUG_AUTOMATION + System.Diagnostics.Debug.WriteLine("DataGridRowsPresenterAutomationPeer.GetClassNameCore returns " + classNameCore); +#endif + return classNameCore; + } + + /// + /// Gets a value that specifies whether the element is a content element. + /// + /// True if the element is a content element; otherwise false + protected override bool IsContentElementCore() + { + return false; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGrid.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGrid.cs new file mode 100644 index 0000000..9d5998a --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGrid.cs @@ -0,0 +1,9190 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Security; +using System.Text; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Data.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.ApplicationModel.DataTransfer; +using Windows.Devices.Input; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.System; +using Windows.UI.Input; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Animation; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Control to represent data in columns and rows. + /// +#if FEATURE_VALIDATION_SUMMARY + [TemplatePart(Name = DataGrid.DATAGRID_elementValidationSummary, Type = typeof(ValidationSummary))] +#endif + [TemplatePart(Name = DataGrid.DATAGRID_elementRowsPresenterName, Type = typeof(DataGridRowsPresenter))] + [TemplatePart(Name = DataGrid.DATAGRID_elementColumnHeadersPresenterName, Type = typeof(DataGridColumnHeadersPresenter))] + [TemplatePart(Name = DataGrid.DATAGRID_elementFrozenColumnScrollBarSpacerName, Type = typeof(FrameworkElement))] + [TemplatePart(Name = DataGrid.DATAGRID_elementHorizontalScrollBarName, Type = typeof(ScrollBar))] + [TemplatePart(Name = DataGrid.DATAGRID_elementVerticalScrollBarName, Type = typeof(ScrollBar))] + [TemplateVisualState(Name = VisualStates.StateDisabled, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StateTouchIndicator, GroupName = VisualStates.GroupScrollBars)] + [TemplateVisualState(Name = VisualStates.StateMouseIndicator, GroupName = VisualStates.GroupScrollBars)] + [TemplateVisualState(Name = VisualStates.StateMouseIndicatorFull, GroupName = VisualStates.GroupScrollBars)] + [TemplateVisualState(Name = VisualStates.StateNoIndicator, GroupName = VisualStates.GroupScrollBars)] + [TemplateVisualState(Name = VisualStates.StateSeparatorExpanded, GroupName = VisualStates.GroupScrollBarsSeparator)] + [TemplateVisualState(Name = VisualStates.StateSeparatorCollapsed, GroupName = VisualStates.GroupScrollBarsSeparator)] + [TemplateVisualState(Name = VisualStates.StateSeparatorExpandedWithoutAnimation, GroupName = VisualStates.GroupScrollBarsSeparator)] + [TemplateVisualState(Name = VisualStates.StateSeparatorCollapsedWithoutAnimation, GroupName = VisualStates.GroupScrollBarsSeparator)] + [TemplateVisualState(Name = VisualStates.StateInvalid, GroupName = VisualStates.GroupValidation)] + [TemplateVisualState(Name = VisualStates.StateValid, GroupName = VisualStates.GroupValidation)] + [StyleTypedProperty(Property = "CellStyle", StyleTargetType = typeof(DataGridCell))] + [StyleTypedProperty(Property = "ColumnHeaderStyle", StyleTargetType = typeof(DataGridColumnHeader))] + [StyleTypedProperty(Property = "DragIndicatorStyle", StyleTargetType = typeof(ContentControl))] + [StyleTypedProperty(Property = "DropLocationIndicatorStyle", StyleTargetType = typeof(Control))] + [StyleTypedProperty(Property = "RowHeaderStyle", StyleTargetType = typeof(DataGridRowHeader))] + [StyleTypedProperty(Property = "RowStyle", StyleTargetType = typeof(DataGridRow))] + public partial class DataGrid : Control + { + private enum ScrollBarVisualState + { + NoIndicator, + TouchIndicator, + MouseIndicator, + MouseIndicatorFull + } + + private enum ScrollBarsSeparatorVisualState + { + SeparatorCollapsed, + SeparatorExpanded, + SeparatorExpandedWithoutAnimation, + SeparatorCollapsedWithoutAnimation + } + +#if FEATURE_VALIDATION_SUMMARY + private const string DATAGRID_elementValidationSummary = "ValidationSummary"; +#endif + private const string DATAGRID_elementRootName = "Root"; + private const string DATAGRID_elementRowsPresenterName = "RowsPresenter"; + private const string DATAGRID_elementColumnHeadersPresenterName = "ColumnHeadersPresenter"; + private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "FrozenColumnScrollBarSpacer"; + private const string DATAGRID_elementHorizontalScrollBarName = "HorizontalScrollBar"; + private const string DATAGRID_elementRowHeadersPresenterName = "RowHeadersPresenter"; + private const string DATAGRID_elementTopLeftCornerHeaderName = "TopLeftCornerHeader"; + private const string DATAGRID_elementTopRightCornerHeaderName = "TopRightCornerHeader"; + private const string DATAGRID_elementBottomRightCornerHeaderName = "BottomRightCorner"; + private const string DATAGRID_elementVerticalScrollBarName = "VerticalScrollBar"; + + private const bool DATAGRID_defaultAutoGenerateColumns = true; + private const bool DATAGRID_defaultCanUserReorderColumns = true; + private const bool DATAGRID_defaultCanUserResizeColumns = true; + private const bool DATAGRID_defaultCanUserSortColumns = true; + private const DataGridGridLinesVisibility DATAGRID_defaultGridLinesVisibility = DataGridGridLinesVisibility.None; + private const DataGridHeadersVisibility DATAGRID_defaultHeadersVisibility = DataGridHeadersVisibility.Column; + private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected; + private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended; + private const ScrollBarVisibility DATAGRID_defaultScrollBarVisibility = ScrollBarVisibility.Auto; + + /// + /// The default order to use for columns when there is no + /// value available for the property. + /// + /// + /// The value of 10,000 comes from the DataAnnotations spec, allowing + /// some properties to be ordered at the beginning and some at the end. + /// + private const int DATAGRID_defaultColumnDisplayOrder = 10000; + + private const double DATAGRID_horizontalGridLinesThickness = 1; + private const double DATAGRID_minimumRowHeaderWidth = 4; + private const double DATAGRID_minimumColumnHeaderHeight = 4; + internal const double DATAGRID_maximumStarColumnWidth = 10000; + internal const double DATAGRID_minimumStarColumnWidth = 0.001; + private const double DATAGRID_mouseWheelDeltaDivider = 4.0; + private const double DATAGRID_maxHeadersThickness = 32768; + + private const double DATAGRID_defaultRowHeight = 22; + internal const double DATAGRID_defaultRowGroupSublevelIndent = 20; + private const double DATAGRID_defaultMinColumnWidth = 20; + private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity; + + private const double DATAGRID_defaultIncrementalLoadingThreshold = 3.0; + private const double DATAGRID_defaultDataFetchSize = 3.0; + + // 2 seconds delay used to hide the scroll bars for example when OS animations are turned off. + private const int DATAGRID_noScrollBarCountdownMs = 2000; + + // Used to work around double arithmetic rounding. + private const double DATAGRID_roundingDelta = 0.0001; + + // DataGrid Template Parts +#if FEATURE_VALIDATION_SUMMARY + private ValidationSummary _validationSummary; +#endif + private UIElement _bottomRightCorner; + private DataGridColumnHeadersPresenter _columnHeadersPresenter; + private ScrollBar _hScrollBar; + private DataGridRowsPresenter _rowsPresenter; + private ScrollBar _vScrollBar; + + private byte _autoGeneratingColumnOperationCount; + private bool _autoSizingColumns; + private List _bindingValidationResults; + private ContentControl _clipboardContentControl; + private IndexToValueTable _collapsedSlotsTable; + private bool _columnHeaderHasFocus; + private DataGridCellCoordinates _currentCellCoordinates; + + // used to store the current column during a Reset + private int _desiredCurrentColumnIndex; + private int _editingColumnIndex; + private RoutedEventArgs _editingEventArgs; + private bool _executingLostFocusActions; + private bool _flushCurrentCellChanged; + private bool _focusEditingControl; + private FocusInputDeviceKind _focusInputDevice; + private DependencyObject _focusedObject; + private DataGridRow _focusedRow; + private FrameworkElement _frozenColumnScrollBarSpacer; + private bool _hasNoIndicatorStateStoryboardCompletedHandler; + private DispatcherTimer _hideScrollBarsTimer; + + // the sum of the widths in pixels of the scrolling columns preceding + // the first displayed scrolling column + private double _horizontalOffset; + private byte _horizontalScrollChangesIgnored; + private bool _ignoreNextScrollBarsLayout; + private List _indeiValidationResults; + private bool _initializingNewItem; + + private bool _isHorizontalScrollBarInteracting; + private bool _isVerticalScrollBarInteracting; + + // Set to True when the pointer is over the optional scroll bars. + private bool _isPointerOverHorizontalScrollBar; + private bool _isPointerOverVerticalScrollBar; + + // Set to True to prevent the normal fade-out of the scroll bars. + private bool _keepScrollBarsShowing; + + // Nth row of rows 0..N that make up the RowHeightEstimate + private int _lastEstimatedRow; + private List _loadedRows; + + // prevents reentry into the VerticalScroll event handler + private Queue _lostFocusActions; + private bool _makeFirstDisplayedCellCurrentCellPending; + private bool _measured; + + // the number of pixels of the firstDisplayedScrollingCol which are not displayed + private double _negHorizontalOffset; + + // the number of pixels of DisplayData.FirstDisplayedScrollingRow which are not displayed + private int _noCurrentCellChangeCount; + private int _noFocusedColumnChangeCount; + private int _noSelectionChangeCount; + + private double _oldEdgedRowsHeightCalculated = 0.0; + + // Set to True to favor mouse indicators over panning indicators for the scroll bars. + private bool _preferMouseIndicators; + + private DataGridCellCoordinates _previousAutomationFocusCoordinates; + private DataGridColumn _previousCurrentColumn; + private object _previousCurrentItem; + private List _propertyValidationResults; + private ScrollBarVisualState _proposedScrollBarsState; + private ScrollBarsSeparatorVisualState _proposedScrollBarsSeparatorState; + private string _rowGroupHeaderPropertyNameAlternative; + private ObservableCollection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None + + + + + + + None + + + + + + + + + + + None + + + + + + + None + + + + + + + + + + + MouseIndicator + + + + + + + MouseIndicator + + + + + + + + + + + None + + + + + + + None + + + + + + + + + + + + + + + TouchIndicator + + + + + + + TouchIndicator + + + + + + + + + + + + MouseIndicator + + + + + + + MouseIndicator + + + + + + + + + + + + MouseIndicator + + + + + + + MouseIndicator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridAutoGeneratingColumnEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridAutoGeneratingColumnEventArgs.cs new file mode 100644 index 0000000..dfcff29 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridAutoGeneratingColumnEventArgs.cs @@ -0,0 +1,61 @@ +// 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; +using System.ComponentModel; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for the event. + /// + public class DataGridAutoGeneratingColumnEventArgs : CancelEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the property bound to the generated column. + /// + /// + /// The of the property bound to the generated column. + /// + /// + /// The generated column. + /// + public DataGridAutoGeneratingColumnEventArgs(string propertyName, Type propertyType, DataGridColumn column) + { + this.Column = column; + this.PropertyName = propertyName; + this.PropertyType = propertyType; + } + + /// + /// Gets or sets the generated column. + /// + public DataGridColumn Column + { + get; + set; + } + + /// + /// Gets the name of the property bound to the generated column. + /// + public string PropertyName + { + get; + private set; + } + + /// + /// Gets the of the property bound to the generated column. + /// + public Type PropertyType + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBeginningEditEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBeginningEditEventArgs.cs new file mode 100644 index 0000000..bc9afbd --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBeginningEditEventArgs.cs @@ -0,0 +1,64 @@ +// 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.ComponentModel; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for the event. + /// + public class DataGridBeginningEditEventArgs : CancelEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The column that contains the cell to be edited. + /// + /// + /// The row that contains the cell to be edited. + /// + /// + /// Information about the user gesture that caused the cell to enter edit mode. + /// + public DataGridBeginningEditEventArgs( + DataGridColumn column, + DataGridRow row, + RoutedEventArgs editingEventArgs) + { + this.Column = column; + this.Row = row; + this.EditingEventArgs = editingEventArgs; + } + + /// + /// Gets the column that contains the cell to be edited. + /// + public DataGridColumn Column + { + get; + private set; + } + + /// + /// Gets information about the user gesture that caused the cell to enter edit mode. + /// + public RoutedEventArgs EditingEventArgs + { + get; + private set; + } + + /// + /// Gets the row that contains the cell to be edited. + /// + public DataGridRow Row + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBoundColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBoundColumn.cs new file mode 100644 index 0000000..7ef0cbf --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridBoundColumn.cs @@ -0,0 +1,212 @@ +// 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.Generic; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Data.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column that can + /// bind to a property in the grid's data source. + /// + [StyleTypedProperty(Property = "ElementStyle", StyleTargetType = typeof(FrameworkElement))] + [StyleTypedProperty(Property = "EditingElementStyle", StyleTargetType = typeof(FrameworkElement))] + public abstract class DataGridBoundColumn : DataGridColumn + { + private Binding _binding; + private Style _elementStyle; + private Style _editingElementStyle; + + /// + /// Gets or sets the binding that associates the column with a property in the data source. + /// + public virtual Binding Binding + { + get + { + return _binding; + } + + set + { + if (_binding != value) + { + if (this.OwningGrid != null && !this.OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/)) + { + // Edited value couldn't be committed, so we force a CancelEdit + this.OwningGrid.CancelEdit(DataGridEditingUnit.Row, false /*raiseEvents*/); + } + + _binding = value; + + if (_binding != null) + { + // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. + if (_binding.Path != null && !string.IsNullOrEmpty(_binding.Path.Path)) + { + _binding.Mode = BindingMode.TwoWay; + } + + if (_binding.Converter == null) + { + _binding.Converter = new DataGridValueConverter(); + } + + // Setup the binding for validation + // Todo: WinUI3, can this be reenabled now? + // _binding.ValidatesOnDataErrors = true; + // _binding.ValidatesOnExceptions = true; + // _binding.NotifyOnValidationError = true; + _binding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit; + + // Apply the new Binding to existing rows in the DataGrid + if (this.OwningGrid != null) + { + // TODO: We want to clear the Bindings if Binding is set to null + // but there's no way to do that right now. Revisit this if UWP + // implements the equivalent of BindingOperations.ClearBinding. + this.OwningGrid.OnColumnBindingChanged(this); + } + } + + this.RemoveEditingElement(); + } + } + } + + /// + /// Gets or sets the binding that will be used to get or set cell content for the clipboard. + /// If the base ClipboardContentBinding is not explicitly set, this will return the value of Binding. + /// + public override Binding ClipboardContentBinding + { + get + { + return base.ClipboardContentBinding ?? this.Binding; + } + + set + { + base.ClipboardContentBinding = value; + } + } + + /// + /// Gets or sets the style that is used when rendering the element that the column displays for a cell in editing mode. + /// + public Style EditingElementStyle + { + get + { + return _editingElementStyle; + } + + set + { + if (_editingElementStyle != value) + { + _editingElementStyle = value; + + // We choose not to update the elements already editing in the Grid here. + // They will get the EditingElementStyle next time they go into edit mode. + } + } + } + + /// + /// Gets or sets the style that is used when rendering the element that the column displays for a cell that is not in editing mode. + /// + public Style ElementStyle + { + get + { + return _elementStyle; + } + + set + { + if (_elementStyle != value) + { + _elementStyle = value; + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnElementStyleChanged(this); + } + } + } + } + + internal DependencyProperty BindingTarget { get; set; } + + internal override List CreateBindingPaths() + { + if (this.Binding != null && this.Binding.Path != null) + { + return new List() { this.Binding.Path.Path }; + } + + return base.CreateBindingPaths(); + } + + internal override List CreateBindings(FrameworkElement element, object dataItem, bool twoWay) + { + BindingInfo bindingData = new BindingInfo(); + if (twoWay && this.BindingTarget != null) + { + bindingData.BindingExpression = element.GetBindingExpression(this.BindingTarget); + if (bindingData.BindingExpression != null) + { + bindingData.BindingTarget = this.BindingTarget; + bindingData.Element = element; + return new List { bindingData }; + } + } + + foreach (DependencyProperty bindingTarget in element.GetDependencyProperties(false)) + { + bindingData.BindingExpression = element.GetBindingExpression(bindingTarget); + if (bindingData.BindingExpression != null + && bindingData.BindingExpression.ParentBinding == this.Binding) + { + this.BindingTarget = bindingTarget; + bindingData.BindingTarget = this.BindingTarget; + bindingData.Element = element; + return new List { bindingData }; + } + } + + return base.CreateBindings(element, dataItem, twoWay); + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + internal override string GetSortPropertyName() + { + if (string.IsNullOrEmpty(this.SortMemberPath) && this.Binding != null && this.Binding.Path != null) + { + return this.Binding.Path.Path; + } + + return this.SortMemberPath; + } +#endif + + internal void SetHeaderFromBinding() + { + if (this.OwningGrid != null && this.OwningGrid.DataConnection.DataType != null && + this.Header == null && this.Binding != null && this.Binding.Path != null) + { + string header = this.OwningGrid.DataConnection.DataType.GetDisplayName(this.Binding.Path.Path); + if (header != null) + { + this.Header = header; + } + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCell.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCell.cs new file mode 100644 index 0000000..73f165a --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCell.cs @@ -0,0 +1,470 @@ +// 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.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Windows.Devices.Input; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Shapes; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents an individual cell. + /// + [TemplatePart(Name = DATAGRIDCELL_elementRightGridLine, Type = typeof(Rectangle))] + + [TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StatePointerOver, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StateUnselected, GroupName = VisualStates.GroupSelection)] + [TemplateVisualState(Name = VisualStates.StateSelected, GroupName = VisualStates.GroupSelection)] + [TemplateVisualState(Name = VisualStates.StateRegular, GroupName = VisualStates.GroupCurrent)] + [TemplateVisualState(Name = VisualStates.StateCurrent, GroupName = VisualStates.GroupCurrent)] + [TemplateVisualState(Name = VisualStates.StateCurrentWithFocus, GroupName = VisualStates.GroupCurrent)] + [TemplateVisualState(Name = VisualStates.StateDisplay, GroupName = VisualStates.GroupInteraction)] + [TemplateVisualState(Name = VisualStates.StateEditing, GroupName = VisualStates.GroupInteraction)] + [TemplateVisualState(Name = VisualStates.StateInvalid, GroupName = VisualStates.GroupValidation)] + [TemplateVisualState(Name = VisualStates.StateValid, GroupName = VisualStates.GroupValidation)] + public sealed partial class DataGridCell : ContentControl + { + private const string DATAGRIDCELL_elementRightGridLine = "RightGridLine"; + + private Rectangle _rightGridLine; + + /// + /// Initializes a new instance of the class. + /// + public DataGridCell() + { + this.IsTapEnabled = true; + this.AddHandler(UIElement.TappedEvent, new TappedEventHandler(DataGridCell_PointerTapped), true /*handledEventsToo*/); + + this.PointerCanceled += new PointerEventHandler(DataGridCell_PointerCanceled); + this.PointerCaptureLost += new PointerEventHandler(DataGridCell_PointerCaptureLost); + this.PointerPressed += new PointerEventHandler(DataGridCell_PointerPressed); + this.PointerReleased += new PointerEventHandler(DataGridCell_PointerReleased); + this.PointerEntered += new PointerEventHandler(DataGridCell_PointerEntered); + this.PointerExited += new PointerEventHandler(DataGridCell_PointerExited); + this.PointerMoved += new PointerEventHandler(DataGridCell_PointerMoved); + + DefaultStyleKey = typeof(DataGridCell); + } + + /// + /// Gets a value indicating whether the data in a cell is valid. + /// + public bool IsValid + { + get + { + return (bool)GetValue(IsValidProperty); + } + + internal set + { + this.SetValueNoCallback(IsValidProperty, value); + } + } + + /// + /// Identifies the IsValid dependency property. + /// + public static readonly DependencyProperty IsValidProperty = + DependencyProperty.Register( + "IsValid", + typeof(bool), + typeof(DataGridCell), + new PropertyMetadata(true, OnIsValidPropertyChanged)); + + /// + /// IsValidProperty property changed handler. + /// + /// DataGridCell that changed its IsValid. + /// DependencyPropertyChangedEventArgs. + private static void OnIsValidPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridCell dataGridCell = d as DataGridCell; + if (!dataGridCell.IsHandlerSuspended(e.Property)) + { + dataGridCell.SetValueNoCallback(DataGridCell.IsValidProperty, e.OldValue); + throw DataGridError.DataGrid.UnderlyingPropertyIsReadOnly("IsValid"); + } + } + + internal double ActualRightGridLineWidth + { + get + { + if (_rightGridLine != null) + { + return _rightGridLine.ActualWidth; + } + + return 0; + } + } + + internal int ColumnIndex + { + get + { + if (this.OwningColumn == null) + { + return -1; + } + + return this.OwningColumn.Index; + } + } + + internal bool IsCurrent + { + get + { + Debug.Assert(this.OwningGrid != null && this.OwningColumn != null && this.OwningRow != null, "Expected non-null owning DataGrid, DataGridColumn and DataGridRow."); + + return this.OwningGrid.CurrentColumnIndex == this.OwningColumn.Index && + this.OwningGrid.CurrentSlot == this.OwningRow.Slot; + } + } + + internal bool IsPointerOver + { + get + { + return this.InteractionInfo != null && this.InteractionInfo.IsPointerOver; + } + + set + { + if (value && this.InteractionInfo == null) + { + this.InteractionInfo = new DataGridInteractionInfo(); + } + + if (this.InteractionInfo != null) + { + this.InteractionInfo.IsPointerOver = value; + } + + ApplyCellState(true /*animate*/); + } + } + + internal DataGridColumn OwningColumn + { + get; + set; + } + + internal DataGrid OwningGrid + { + get + { + if (this.OwningRow != null && this.OwningRow.OwningGrid != null) + { + return this.OwningRow.OwningGrid; + } + + if (this.OwningColumn != null) + { + return this.OwningColumn.OwningGrid; + } + + return null; + } + } + + internal DataGridRow OwningRow + { + get; + set; + } + + internal int RowIndex + { + get + { + if (this.OwningRow == null) + { + return -1; + } + + return this.OwningRow.Index; + } + } + + private DataGridInteractionInfo InteractionInfo + { + get; + set; + } + + private bool IsEdited + { + get + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + return this.OwningGrid.EditingRow == this.OwningRow && + this.OwningGrid.EditingColumnIndex == this.ColumnIndex; + } + } + + /// + /// Builds the visual tree for the row header when a new template is applied. + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + ApplyCellState(false /*animate*/); + + _rightGridLine = GetTemplateChild(DATAGRIDCELL_elementRightGridLine) as Rectangle; + if (_rightGridLine != null && this.OwningColumn == null) + { + // Turn off the right GridLine for filler cells + _rightGridLine.Visibility = Visibility.Collapsed; + } + else + { + EnsureGridLine(null); + } + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + if (this.OwningGrid != null && + this.OwningColumn != null && + this.OwningColumn != this.OwningGrid.ColumnsInternal.FillerColumn) + { + return new DataGridCellAutomationPeer(this); + } + + return base.OnCreateAutomationPeer(); + } + + internal void ApplyCellState(bool animate) + { + if (this.OwningGrid == null || this.OwningColumn == null || this.OwningRow == null || this.OwningRow.Visibility == Visibility.Collapsed || this.OwningRow.Slot == -1) + { + return; + } + + // CommonStates + if (this.IsPointerOver) + { + VisualStates.GoToState(this, animate, VisualStates.StatePointerOver, VisualStates.StateNormal); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateNormal); + } + + // SelectionStates + if (this.OwningRow.IsSelected) + { + VisualStates.GoToState(this, animate, VisualStates.StateSelected, VisualStates.StateUnselected); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateUnselected); + } + + // CurrentStates + if (this.IsCurrent && !this.OwningGrid.ColumnHeaderHasFocus) + { + if (this.OwningGrid.ContainsFocus) + { + VisualStates.GoToState(this, animate, VisualStates.StateCurrentWithFocus, VisualStates.StateCurrent, VisualStates.StateRegular); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateCurrent, VisualStates.StateRegular); + } + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateRegular); + } + + // Interaction states + if (this.IsEdited) + { + VisualStates.GoToState(this, animate, VisualStates.StateEditing, VisualStates.StateDisplay); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateDisplay); + } + + // Validation states + if (this.IsValid) + { + VisualStates.GoToState(this, animate, VisualStates.StateValid); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateInvalid, VisualStates.StateValid); + } + } + + // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the + // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column + internal void EnsureGridLine(DataGridColumn lastVisibleColumn) + { + if (this.OwningGrid != null && _rightGridLine != null) + { + if (!(this.OwningColumn is DataGridFillerColumn) && this.OwningGrid.VerticalGridLinesBrush != null && this.OwningGrid.VerticalGridLinesBrush != _rightGridLine.Fill) + { + _rightGridLine.Fill = this.OwningGrid.VerticalGridLinesBrush; + } + + Visibility newVisibility = + (this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Vertical || this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All) && + (this.OwningGrid.ColumnsInternal.FillerColumn.IsActive || this.OwningColumn != lastVisibleColumn) + ? Visibility.Visible : Visibility.Collapsed; + + if (newVisibility != _rightGridLine.Visibility) + { + _rightGridLine.Visibility = newVisibility; + } + } + } + + /// + /// Ensures that the correct Style is applied to this object. + /// + /// Caller's previous associated Style + internal void EnsureStyle(Style previousStyle) + { + if (this.Style != null && + (this.OwningColumn == null || this.Style != this.OwningColumn.CellStyle) && + (this.OwningGrid == null || this.Style != this.OwningGrid.CellStyle) && + this.Style != previousStyle) + { + return; + } + + Style style = null; + if (this.OwningColumn != null) + { + style = this.OwningColumn.CellStyle; + } + + if (style == null && this.OwningGrid != null) + { + style = this.OwningGrid.CellStyle; + } + + this.SetStyleWithType(style); + } + + internal void Recycle() + { + this.InteractionInfo = null; + } + + private void CancelPointer(PointerRoutedEventArgs e) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId == e.Pointer.PointerId) + { + this.InteractionInfo.CapturedPointerId = 0u; + } + + this.IsPointerOver = false; + } + + private void DataGridCell_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void DataGridCell_PointerCaptureLost(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void DataGridCell_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch && + this.OwningGrid != null && + this.OwningGrid.AllowsManipulation && + (this.InteractionInfo == null || this.InteractionInfo.CapturedPointerId == 0u) && + this.CapturePointer(e.Pointer)) + { + if (this.InteractionInfo == null) + { + this.InteractionInfo = new DataGridInteractionInfo(); + } + + this.InteractionInfo.CapturedPointerId = e.Pointer.PointerId; + } + } + + private void DataGridCell_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId == e.Pointer.PointerId) + { + ReleasePointerCapture(e.Pointer); + } + } + + private void DataGridCell_PointerEntered(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridCell_PointerExited(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(false); + } + + private void DataGridCell_PointerMoved(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridCell_PointerTapped(object sender, TappedRoutedEventArgs e) + { + // OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow + if (this.OwningGrid != null && !this.OwningGrid.HasColumnUserInteraction) + { + if (!e.Handled && this.OwningGrid.IsTabStop) + { + bool success = this.OwningGrid.Focus(FocusState.Programmatic); + Debug.Assert(success, "Expected successful focus change."); + } + + if (this.OwningRow != null) + { + Debug.Assert(sender is DataGridCell, "Expected sender is DataGridCell."); + Debug.Assert(sender as ContentControl == this, "Expected sender is this."); + e.Handled = this.OwningGrid.UpdateStateOnTapped(e, this.ColumnIndex, this.OwningRow.Slot, !e.Handled /*allowEdit*/); + this.OwningGrid.UpdatedStateOnTapped = true; + } + } + } + + private void UpdateIsPointerOver(bool isPointerOver) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId != 0u) + { + return; + } + + this.IsPointerOver = isPointerOver; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCollection.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCollection.cs new file mode 100644 index 0000000..db6a1c1 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCollection.cs @@ -0,0 +1,79 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + internal class DataGridCellCollection + { + private List _cells; + private DataGridRow _owningRow; + + internal event EventHandler CellAdded; + + internal event EventHandler CellRemoved; + + public DataGridCellCollection(DataGridRow owningRow) + { + _owningRow = owningRow; + _cells = new List(); + } + + public int Count + { + get + { + return _cells.Count; + } + } + + public IEnumerator GetEnumerator() + { + return _cells.GetEnumerator(); + } + + public void Insert(int cellIndex, DataGridCell cell) + { + Debug.Assert(cellIndex >= 0 && cellIndex <= _cells.Count, "Expected cellIndex between 0 and _cells.Count inclusive."); + Debug.Assert(cell != null, "Expected non-null cell."); + + cell.OwningRow = _owningRow; + _cells.Insert(cellIndex, cell); + + if (CellAdded != null) + { + CellAdded(this, new DataGridCellEventArgs(cell)); + } + } + + public void RemoveAt(int cellIndex) + { + DataGridCell dataGridCell = _cells[cellIndex]; + _cells.RemoveAt(cellIndex); + dataGridCell.OwningRow = null; + if (CellRemoved != null) + { + CellRemoved(this, new DataGridCellEventArgs(dataGridCell)); + } + } + + public DataGridCell this[int index] + { + get + { + if (index < 0 || index >= _cells.Count) + { + throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _cells.Count, false); + } + + return _cells[index]; + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCoordinates.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCoordinates.cs new file mode 100644 index 0000000..f94901a --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellCoordinates.cs @@ -0,0 +1,59 @@ +// 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.Globalization; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridCellCoordinates + { + public DataGridCellCoordinates(int columnIndex, int slot) + { + this.ColumnIndex = columnIndex; + this.Slot = slot; + } + + public DataGridCellCoordinates(DataGridCellCoordinates dataGridCellCoordinates) + : this(dataGridCellCoordinates.ColumnIndex, dataGridCellCoordinates.Slot) + { + } + + public int ColumnIndex + { + get; + set; + } + + public int Slot + { + get; + set; + } + + public override bool Equals(object o) + { + DataGridCellCoordinates dataGridCellCoordinates = o as DataGridCellCoordinates; + if (dataGridCellCoordinates != null) + { + return dataGridCellCoordinates.ColumnIndex == this.ColumnIndex && dataGridCellCoordinates.Slot == this.Slot; + } + + return false; + } + + // Avoiding build warning CS0659: 'DataGridCellCoordinates' overrides Object.Equals(object o) but does not override Object.GetHashCode() + public override int GetHashCode() + { + return base.GetHashCode(); + } + +#if DEBUG + public override string ToString() + { + return "DataGridCellCoordinates {ColumnIndex = " + this.ColumnIndex.ToString(CultureInfo.CurrentCulture) + + ", Slot = " + this.Slot.ToString(CultureInfo.CurrentCulture) + "}"; + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndedEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndedEventArgs.cs new file mode 100644 index 0000000..0d00fed --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndedEventArgs.cs @@ -0,0 +1,54 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides information just after a cell has exited editing mode. + /// + public class DataGridCellEditEndedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The column of the cell that has just exited edit mode. + /// The row container of the cell container that has just exited edit mode. + /// The editing action that has been taken. + public DataGridCellEditEndedEventArgs(DataGridColumn column, DataGridRow row, DataGridEditAction editAction) + { + this.Column = column; + this.Row = row; + this.EditAction = editAction; + } + + /// + /// Gets the column of the cell that has just exited edit mode. + /// + public DataGridColumn Column + { + get; + private set; + } + + /// + /// Gets the edit action taken when leaving edit mode. + /// + public DataGridEditAction EditAction + { + get; + private set; + } + + /// + /// Gets the row container of the cell container that has just exited edit mode. + /// + public DataGridRow Row + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndingEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndingEventArgs.cs new file mode 100644 index 0000000..01b9c73 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEditEndingEventArgs.cs @@ -0,0 +1,70 @@ +// 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.ComponentModel; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides information just before a cell exits editing mode. + /// + public class DataGridCellEditEndingEventArgs : CancelEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The column of the cell that is about to exit edit mode. + /// The row container of the cell container that is about to exit edit mode. + /// The editing element within the cell. + /// The editing action that will be taken. + public DataGridCellEditEndingEventArgs( + DataGridColumn column, + DataGridRow row, + FrameworkElement editingElement, + DataGridEditAction editAction) + { + this.Column = column; + this.Row = row; + this.EditingElement = editingElement; + this.EditAction = editAction; + } + + /// + /// Gets the column of the cell that is about to exit edit mode. + /// + public DataGridColumn Column + { + get; + private set; + } + + /// + /// Gets the edit action to take when leaving edit mode. + /// + public DataGridEditAction EditAction + { + get; + private set; + } + + /// + /// Gets the editing element within the cell. + /// + public FrameworkElement EditingElement + { + get; + private set; + } + + /// + /// Gets the row container of the cell container that is about to exit edit mode. + /// + public DataGridRow Row + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEventArgs.cs new file mode 100644 index 0000000..b22f7a7 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellEventArgs.cs @@ -0,0 +1,25 @@ +// 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; +using System.Diagnostics; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + internal class DataGridCellEventArgs : EventArgs + { + internal DataGridCellEventArgs(DataGridCell dataGridCell) + { + Debug.Assert(dataGridCell != null, "Expected non-null dataGridCell parameter."); + + this.Cell = dataGridCell; + } + + internal DataGridCell Cell + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellsPresenter.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellsPresenter.cs new file mode 100644 index 0000000..f7409ce --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCellsPresenter.cs @@ -0,0 +1,338 @@ +// 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; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Used within the template of a + /// to specify the location in the control's visual tree where the cells are to be added. + /// + public sealed class DataGridCellsPresenter : Panel + { + private double _fillerLeftEdge; + + // The desired height needs to be cached due to column virtualization; otherwise, the cells + // would grow and shrink as the DataGrid scrolls horizontally + private double DesiredHeight + { + get; + set; + } + + private DataGrid OwningGrid + { + get + { + if (this.OwningRow != null) + { + return this.OwningRow.OwningGrid; + } + + return null; + } + } + + internal DataGridRow OwningRow + { + get; + set; + } + + /// + /// Arranges the content of the . + /// + /// + /// The actual size used by the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + if (this.OwningGrid.AutoSizingColumns) + { + // When we initially load an auto-column, we have to wait for all the rows to be measured + // before we know its final desired size. We need to trigger a new round of measures now + // that the final sizes have been calculated. + this.OwningGrid.AutoSizingColumns = false; + return base.ArrangeOverride(finalSize); + } + + double frozenLeftEdge = 0; + double scrollingLeftEdge = -this.OwningGrid.HorizontalOffset; + + double cellLeftEdge; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + DataGridCell cell = this.OwningRow.Cells[column.Index]; + Debug.Assert(cell.OwningColumn == column, "Expected column owner."); + Debug.Assert(column.IsVisible, "Expected visible column."); + + if (column.IsFrozen) + { + cellLeftEdge = frozenLeftEdge; + + // This can happen before or after clipping because frozen cells aren't clipped + frozenLeftEdge += column.ActualWidth; + } + else + { + cellLeftEdge = scrollingLeftEdge; + } + + if (cell.Visibility == Visibility.Visible) + { + cell.Arrange(new Rect(cellLeftEdge, 0, column.LayoutRoundedWidth, finalSize.Height)); + EnsureCellClip(cell, column.ActualWidth, finalSize.Height, frozenLeftEdge, scrollingLeftEdge); + } + + scrollingLeftEdge += column.ActualWidth; + column.IsInitialDesiredWidthDetermined = true; + } + + _fillerLeftEdge = scrollingLeftEdge; + + // FillerColumn.Width == 0 when the filler column is not active + this.OwningRow.FillerCell.Arrange(new Rect(_fillerLeftEdge, 0, this.OwningGrid.ColumnsInternal.FillerColumn.FillerWidth, finalSize.Height)); + + return finalSize; + } + + private static void EnsureCellClip(DataGridCell cell, double width, double height, double frozenLeftEdge, double cellLeftEdge) + { + // Clip the cell only if it's scrolled under frozen columns. Unfortunately, we need to clip in this case + // because cells could be transparent + if (!cell.OwningColumn.IsFrozen && frozenLeftEdge > cellLeftEdge) + { + RectangleGeometry rg = new RectangleGeometry(); + double xClip = Math.Round(Math.Min(width, frozenLeftEdge - cellLeftEdge)); + rg.Rect = new Rect(xClip, 0, Math.Max(0, width - xClip), height); + cell.Clip = rg; + } + else + { + cell.Clip = null; + } + } + + private static void EnsureCellDisplay(DataGridCell cell, bool displayColumn) + { + if (cell.IsCurrent) + { + if (displayColumn) + { + cell.Visibility = Visibility.Visible; + cell.Clip = null; + } + else + { + // Clip + RectangleGeometry rg = new RectangleGeometry(); + rg.Rect = Rect.Empty; + cell.Clip = rg; + } + } + else + { + cell.Visibility = displayColumn ? Visibility.Visible : Visibility.Collapsed; + } + } + + internal void EnsureFillerVisibility() + { + DataGridFillerColumn fillerColumn = this.OwningGrid.ColumnsInternal.FillerColumn; + Visibility newVisibility = fillerColumn.IsActive ? Visibility.Visible : Visibility.Collapsed; + if (this.OwningRow.FillerCell.Visibility != newVisibility) + { + this.OwningRow.FillerCell.Visibility = newVisibility; + if (newVisibility == Visibility.Visible) + { + this.OwningRow.FillerCell.Arrange(new Rect(_fillerLeftEdge, 0, fillerColumn.FillerWidth, this.ActualHeight)); + } + } + + // This must be done after the Filler visibility is determined. This also must be done + // regardless of whether or not the filler visibility actually changed values because + // we could scroll in a cell that didn't have EnsureGridLine called yet + DataGridColumn lastVisibleColumn = this.OwningGrid.ColumnsInternal.LastVisibleColumn; + if (lastVisibleColumn != null) + { + DataGridCell cell = this.OwningRow.Cells[lastVisibleColumn.Index]; + cell.EnsureGridLine(lastVisibleColumn); + } + } + + /// + /// Measures the children of a to + /// prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (this.OwningGrid == null) + { + return base.MeasureOverride(availableSize); + } + + bool autoSizeHeight; + double measureHeight; + if (double.IsNaN(this.OwningGrid.RowHeight)) + { + // No explicit height values were set so we can autosize + autoSizeHeight = true; + measureHeight = double.PositiveInfinity; + } + else + { + this.DesiredHeight = this.OwningGrid.RowHeight; + measureHeight = this.DesiredHeight; + autoSizeHeight = false; + } + + double frozenLeftEdge = 0; + double totalDisplayWidth = 0; + double scrollingLeftEdge = -this.OwningGrid.HorizontalOffset; + this.OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + DataGridColumn lastVisibleColumn = this.OwningGrid.ColumnsInternal.LastVisibleColumn; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + DataGridCell cell = this.OwningRow.Cells[column.Index]; + + // Measure the entire first row to make the horizontal scrollbar more accurate + bool shouldDisplayCell = ShouldDisplayCell(column, frozenLeftEdge, scrollingLeftEdge) || this.OwningRow.Index == 0; + EnsureCellDisplay(cell, shouldDisplayCell); + if (shouldDisplayCell) + { + DataGridLength columnWidth = column.Width; + bool autoGrowWidth = columnWidth.IsSizeToCells || columnWidth.IsAuto; + if (column != lastVisibleColumn) + { + cell.EnsureGridLine(lastVisibleColumn); + } + + // If we're not using star sizing or the current column can't be resized, + // then just set the display width according to the column's desired width + if (!this.OwningGrid.UsesStarSizing || (!column.ActualCanUserResize && !column.Width.IsStar)) + { + // In the edge-case where we're given infinite width and we have star columns, the + // star columns grow to their predefined limit of 10,000 (or their MaxWidth) + double newDisplayWidth = column.Width.IsStar ? + Math.Min(column.ActualMaxWidth, DataGrid.DATAGRID_maximumStarColumnWidth) : + Math.Max(column.ActualMinWidth, Math.Min(column.ActualMaxWidth, column.Width.DesiredValue)); + column.SetWidthDisplayValue(newDisplayWidth); + } + + // If we're auto-growing the column based on the cell content, we want to measure it at its maximum value + if (autoGrowWidth) + { + cell.Measure(new Size(column.ActualMaxWidth, measureHeight)); + this.OwningGrid.AutoSizeColumn(column, cell.DesiredSize.Width); + column.ComputeLayoutRoundedWidth(totalDisplayWidth); + } + else if (!this.OwningGrid.UsesStarSizing) + { + column.ComputeLayoutRoundedWidth(scrollingLeftEdge); + cell.Measure(new Size(column.LayoutRoundedWidth, measureHeight)); + } + + // We need to track the largest height in order to auto-size + if (autoSizeHeight) + { + this.DesiredHeight = Math.Max(this.DesiredHeight, cell.DesiredSize.Height); + } + } + + if (column.IsFrozen) + { + frozenLeftEdge += column.ActualWidth; + } + + scrollingLeftEdge += column.ActualWidth; + totalDisplayWidth += column.ActualWidth; + } + + // If we're using star sizing (and we're not waiting for an auto-column to finish growing) + // then we will resize all the columns to fit the available space. + if (this.OwningGrid.UsesStarSizing && !this.OwningGrid.AutoSizingColumns) + { + double adjustment = this.OwningGrid.CellsWidth - totalDisplayWidth; + this.OwningGrid.AdjustColumnWidths(0, adjustment, false); + + // Since we didn't know the final widths of the columns until we resized, + // we waited until now to measure each cell + double leftEdge = 0; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + DataGridCell cell = this.OwningRow.Cells[column.Index]; + column.ComputeLayoutRoundedWidth(leftEdge); + cell.Measure(new Size(column.LayoutRoundedWidth, measureHeight)); + if (autoSizeHeight) + { + this.DesiredHeight = Math.Max(this.DesiredHeight, cell.DesiredSize.Height); + } + + leftEdge += column.ActualWidth; + } + } + + // Measure FillerCell, we're doing it unconditionally here because we don't know if we'll need the filler + // column and we don't want to cause another Measure if we do + this.OwningRow.FillerCell.Measure(new Size(double.PositiveInfinity, this.DesiredHeight)); + + this.OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + return new Size(this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, this.DesiredHeight); + } + + internal void Recycle() + { + // Clear out the cached desired height so it is not reused for other rows + this.DesiredHeight = 0; + + if (this.OwningGrid != null && this.OwningRow != null) + { + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal) + { + this.OwningRow.Cells[column.Index].Recycle(); + } + } + } + + private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + Debug.Assert(this.OwningGrid.HorizontalAdjustment >= 0, "Expected owning positive DataGrid.HorizontalAdjustment."); + Debug.Assert(this.OwningGrid.HorizontalAdjustment <= this.OwningGrid.HorizontalOffset, "Expected owning DataGrid.HorizontalAdjustment smaller than or equal to DataGrid.HorizontalOffset."); + + if (column.Visibility != Visibility.Visible) + { + return false; + } + + scrollingLeftEdge += this.OwningGrid.HorizontalAdjustment; + double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge; + double rightEdge = leftEdge + column.ActualWidth; + return DoubleUtil.GreaterThan(rightEdge, 0) && + DoubleUtil.LessThanOrClose(leftEdge, this.OwningGrid.CellsWidth) && + DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCheckBoxColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCheckBoxColumn.cs new file mode 100644 index 0000000..3663435 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridCheckBoxColumn.cs @@ -0,0 +1,361 @@ +// 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; +using System.Collections.Specialized; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column that hosts + /// controls in its cells. + /// + [StyleTypedProperty(Property = "ElementStyle", StyleTargetType = typeof(CheckBox))] + [StyleTypedProperty(Property = "EditingElementStyle", StyleTargetType = typeof(CheckBox))] + public class DataGridCheckBoxColumn : DataGridBoundColumn + { + private const string DATAGRIDCHECKBOXCOLUMN_isThreeStateName = "IsThreeState"; + private const double DATAGRIDCHECKBOXCOLUMN_leftMargin = 12.0; + + private bool _beganEditWithKeyboard; + private bool _isThreeState; + private CheckBox _currentCheckBox; + private DataGrid _owningGrid; + + /// + /// Initializes a new instance of the class. + /// + public DataGridCheckBoxColumn() + { + this.BindingTarget = CheckBox.IsCheckedProperty; + } + + /// + /// Gets or sets a value indicating whether the hosted controls allow three states or two. + /// + /// + /// true if the hosted controls support three states; false if they support two states. The default is false. + /// + public bool IsThreeState + { + get + { + return _isThreeState; + } + + set + { + if (_isThreeState != value) + { + _isThreeState = value; + NotifyPropertyChanged(DATAGRIDCHECKBOXCOLUMN_isThreeStateName); + } + } + } + + /// + /// Causes the column cell being edited to revert to the specified value. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// The previous, unedited value in the cell being edited. + /// + protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue) + { + CheckBox editingCheckBox = editingElement as CheckBox; + if (editingCheckBox != null) + { + editingCheckBox.IsChecked = (bool?)uneditedValue; + } + } + + /// + /// Gets a control that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new control that is bound to the column's property value. + /// + protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) + { + CheckBox checkBox = new CheckBox(); + ConfigureCheckBox(checkBox, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + return checkBox; + } + + /// + /// Gets a read-only control that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new, read-only control that is bound to the column's property value. + /// + protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) + { + bool isEnabled = false; + CheckBox checkBoxElement = new CheckBox(); + if (EnsureOwningGrid()) + { + if (cell.RowIndex != -1 && cell.ColumnIndex != -1 && + cell.OwningRow != null && + cell.OwningRow.Slot == this.OwningGrid.CurrentSlot && + cell.ColumnIndex == this.OwningGrid.CurrentColumnIndex) + { + isEnabled = true; + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + + _currentCheckBox = checkBoxElement; + } + } + + checkBoxElement.IsEnabled = isEnabled; + checkBoxElement.IsHitTestVisible = false; + checkBoxElement.IsTabStop = false; + ConfigureCheckBox(checkBoxElement, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + return checkBoxElement; + } + + /// + /// Called when a cell in the column enters editing mode. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// Information about the user gesture that is causing a cell to enter editing mode. + /// + /// + /// The unedited value. + /// + protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + CheckBox editingCheckBox = editingElement as CheckBox; + if (editingCheckBox != null) + { + bool? uneditedValue = editingCheckBox.IsChecked; + + PointerRoutedEventArgs pointerEventArgs = editingEventArgs as PointerRoutedEventArgs; + bool editValue = false; + if (pointerEventArgs != null) + { + // Editing was triggered by a mouse click + Point position = pointerEventArgs.GetCurrentPoint(editingCheckBox).Position; + Rect rect = new Rect(0, 0, editingCheckBox.ActualWidth, editingCheckBox.ActualHeight); + editValue = rect.Contains(position); + } + else if (_beganEditWithKeyboard) + { + // Editing began by a user pressing spacebar + editValue = true; + _beganEditWithKeyboard = false; + } + + if (editValue) + { + // User clicked the checkbox itself or pressed space, let's toggle the IsChecked value + if (editingCheckBox.IsThreeState) + { + switch (editingCheckBox.IsChecked) + { + case false: + editingCheckBox.IsChecked = true; + break; + case true: + editingCheckBox.IsChecked = null; + break; + case null: + editingCheckBox.IsChecked = false; + break; + } + } + else + { + editingCheckBox.IsChecked = !editingCheckBox.IsChecked; + } + } + + return uneditedValue; + } + + return false; + } + + /// + /// Called by the DataGrid control when this column asks for its elements to be + /// updated, because its CheckBoxContent or IsThreeState property changed. + /// + protected internal override void RefreshCellContent(FrameworkElement element, Brush computedRowForeground, string propertyName) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + CheckBox checkBox = element as CheckBox; + if (checkBox == null) + { + throw DataGridError.DataGrid.ValueIsNotAnInstanceOf("element", typeof(CheckBox)); + } + + if (propertyName == DATAGRIDCHECKBOXCOLUMN_isThreeStateName) + { + checkBox.IsThreeState = this.IsThreeState; + } + else + { + checkBox.IsThreeState = this.IsThreeState; + checkBox.Foreground = computedRowForeground; + } + } + + /// + /// Called when the computed foreground of a row changed. + /// + protected internal override void RefreshForeground(FrameworkElement element, Brush computedRowForeground) + { + CheckBox checkBox = element as CheckBox; + if (checkBox != null) + { + checkBox.Foreground = computedRowForeground; + } + } + + private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.OwningGrid == null && _owningGrid != null) + { + _owningGrid.Columns.CollectionChanged -= new NotifyCollectionChangedEventHandler(Columns_CollectionChanged); + _owningGrid.CurrentCellChanged -= new EventHandler(OwningGrid_CurrentCellChanged); + _owningGrid.KeyDown -= new KeyEventHandler(OwningGrid_KeyDown); + _owningGrid.LoadingRow -= new EventHandler(OwningGrid_LoadingRow); + _owningGrid = null; + } + } + + private void ConfigureCheckBox(CheckBox checkBox, Brush computedRowForeground) + { + checkBox.Margin = new Thickness(DATAGRIDCHECKBOXCOLUMN_leftMargin, 0.0, 0.0, 0.0); + checkBox.HorizontalAlignment = HorizontalAlignment.Left; + checkBox.VerticalAlignment = VerticalAlignment.Center; + checkBox.IsThreeState = this.IsThreeState; + checkBox.UseSystemFocusVisuals = false; + checkBox.Foreground = computedRowForeground; + if (this.Binding != null) + { + checkBox.SetBinding(this.BindingTarget, this.Binding); + } + } + + private bool EnsureOwningGrid() + { + if (this.OwningGrid != null) + { + if (this.OwningGrid != _owningGrid) + { + _owningGrid = this.OwningGrid; + _owningGrid.Columns.CollectionChanged += new NotifyCollectionChangedEventHandler(Columns_CollectionChanged); + _owningGrid.CurrentCellChanged += new EventHandler(OwningGrid_CurrentCellChanged); + _owningGrid.KeyDown += new KeyEventHandler(OwningGrid_KeyDown); + _owningGrid.LoadingRow += new EventHandler(OwningGrid_LoadingRow); + } + + return true; + } + + return false; + } + + private void OwningGrid_CurrentCellChanged(object sender, EventArgs e) + { + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + + if (this.OwningGrid != null && this.OwningGrid.CurrentColumn == this && + this.OwningGrid.IsSlotVisible(this.OwningGrid.CurrentSlot)) + { + DataGridRow row = this.OwningGrid.DisplayData.GetDisplayedElement(this.OwningGrid.CurrentSlot) as DataGridRow; + if (row != null) + { + CheckBox checkBox = this.GetCellContent(row) as CheckBox; + if (checkBox != null) + { + checkBox.IsEnabled = true; + } + + _currentCheckBox = checkBox; + } + } + } + + private void OwningGrid_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Space && + this.OwningGrid != null && + this.OwningGrid.CurrentColumn == this) + { + DataGridRow row = this.OwningGrid.DisplayData.GetDisplayedElement(this.OwningGrid.CurrentSlot) as DataGridRow; + if (row != null) + { + CheckBox checkBox = this.GetCellContent(row) as CheckBox; + if (checkBox == _currentCheckBox) + { + _beganEditWithKeyboard = true; + this.OwningGrid.BeginEdit(); + return; + } + } + } + + _beganEditWithKeyboard = false; + } + + private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) + { + if (this.OwningGrid != null) + { + CheckBox checkBox = this.GetCellContent(e.Row) as CheckBox; + if (checkBox != null) + { + if (this.OwningGrid.CurrentColumnIndex == this.Index && this.OwningGrid.CurrentSlot == e.Row.Slot) + { + if (_currentCheckBox != null) + { + _currentCheckBox.IsEnabled = false; + } + + checkBox.IsEnabled = true; + _currentCheckBox = checkBox; + } + else + { + checkBox.IsEnabled = false; + } + } + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridClipboardCellContent.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridClipboardCellContent.cs new file mode 100644 index 0000000..b635e8f --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridClipboardCellContent.cs @@ -0,0 +1,114 @@ +// 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. + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// This structure encapsulate the cell information necessary when clipboard content is prepared. + /// + public struct DataGridClipboardCellContent + { + private DataGridColumn _column; + private object _content; + private object _item; + + /// + /// Initializes a new instance of the struct. + /// + /// DataGrid row item containing the cell. + /// DataGridColumn containing the cell. + /// DataGrid cell value. + public DataGridClipboardCellContent(object item, DataGridColumn column, object content) + { + _item = item; + _column = column; + _content = content; + } + + /// + /// Gets the column containing the cell. + /// + public DataGridColumn Column + { + get + { + return _column; + } + } + + /// + /// Gets the cell content. + /// + public object Content + { + get + { + return _content; + } + } + + /// + /// Gets the row item containing the cell. + /// + public object Item + { + get + { + return _item; + } + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// DataGridClipboardCellContent to compare. + /// True if this and data are equal + public override bool Equals(object obj) + { + if (!(obj is DataGridClipboardCellContent)) + { + return false; + } + + DataGridClipboardCellContent clipboardCellContent = (DataGridClipboardCellContent)obj; + return _column == clipboardCellContent._column && _content == clipboardCellContent._content && _item == clipboardCellContent._item; + } + + /// + /// Returns a deterministic hash code. + /// + /// Hash value. + public override int GetHashCode() + { + return (_column.GetHashCode() ^ _content.GetHashCode()) ^ _item.GetHashCode(); + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// The first DataGridClipboardCellContent. + /// The second DataGridClipboardCellContent. + /// True if and only if clipboardCellContent1 and clipboardCellContent2 are equal. + public static bool operator ==(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2) + { + return clipboardCellContent1._column == clipboardCellContent2._column && clipboardCellContent1._content == clipboardCellContent2._content && clipboardCellContent1._item == clipboardCellContent2._item; + } + + /// + /// Field-by-field comparison to avoid reflection-based ValueType.Equals. + /// + /// The first DataGridClipboardCellContent. + /// The second DataGridClipboardCellContent. + /// True if clipboardCellContent1 and clipboardCellContent2 are NOT equal. + public static bool operator !=(DataGridClipboardCellContent clipboardCellContent1, DataGridClipboardCellContent clipboardCellContent2) + { + if (clipboardCellContent1._column == clipboardCellContent2._column && clipboardCellContent1._content == clipboardCellContent2._content) + { + return clipboardCellContent1._item != clipboardCellContent2._item; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumn.cs new file mode 100644 index 0000000..c0c5258 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumn.cs @@ -0,0 +1,1417 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Microsoft.Toolkit.Uwp.UI.Data.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column. + /// + [StyleTypedProperty(Property = "CellStyle", StyleTargetType = typeof(DataGridCell))] + [StyleTypedProperty(Property = "DragIndicatorStyle", StyleTargetType = typeof(ContentControl))] + [StyleTypedProperty(Property = "HeaderStyle", StyleTargetType = typeof(DataGridColumnHeader))] + public abstract class DataGridColumn : DependencyObject + { + private const bool DATAGRIDCOLUMN_defaultCanUserReorder = true; + private const bool DATAGRIDCOLUMN_defaultCanUserResize = true; + private const bool DATAGRIDCOLUMN_defaultCanUserSort = true; + private const bool DATAGRIDCOLUMN_defaultIsReadOnly = false; + + private List _bindingPaths; + private Style _cellStyle; + private Binding _clipboardContentBinding; + private int _displayIndexWithFiller; + private Style _dragIndicatorStyle; + private FrameworkElement _editingElement; + private object _header; + private DataGridColumnHeader _headerCell; + private Style _headerStyle; + private List _inputBindings; + private bool? _isReadOnly; + private double? _maxWidth; + private double? _minWidth; + private bool _settingWidthInternally; + private DataGridLength? _width; // Null by default, null means inherit the Width from the DataGrid + private Visibility _visibility; + private DataGridSortDirection? _sortDirection; + + /// + /// Initializes a new instance of the class. + /// + protected internal DataGridColumn() + { + _visibility = Visibility.Visible; + _displayIndexWithFiller = -1; + this.IsInitialDesiredWidthDetermined = false; + this.InheritsWidth = true; + } + + /// + /// Gets the actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level + /// have been taken into account. + /// + public double ActualWidth + { + get + { + if (this.OwningGrid == null || double.IsNaN(this.Width.DisplayValue)) + { + return this.ActualMinWidth; + } + + return this.Width.DisplayValue; + } + } + + /// + /// Gets or sets a value indicating whether the user can change the column display position by dragging the column header. + /// + /// + /// true if the user can drag the column header to a new position; otherwise, false. The default is the current property value. + /// + public bool CanUserReorder + { + get + { + if (this.CanUserReorderInternal.HasValue) + { + return this.CanUserReorderInternal.Value; + } + else if (this.OwningGrid != null) + { + return this.OwningGrid.CanUserReorderColumns; + } + else + { + return DATAGRIDCOLUMN_defaultCanUserReorder; + } + } + + set + { + this.CanUserReorderInternal = value; + } + } + + /// + /// Gets or sets a value indicating whether the user can adjust the column width using the mouse. + /// + /// + /// True if the user can resize the column; false if the user cannot resize the column. The default is the current property value. + /// + public bool CanUserResize + { + get + { + if (this.CanUserResizeInternal.HasValue) + { + return this.CanUserResizeInternal.Value; + } + else if (this.OwningGrid != null) + { + return this.OwningGrid.CanUserResizeColumns; + } + else + { + return DATAGRIDCOLUMN_defaultCanUserResize; + } + } + + set + { + this.CanUserResizeInternal = value; + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnCanUserResizeChanged(this); + } + } + } + + /// + /// Gets or sets a value indicating whether the user can sort the column by clicking the column header. + /// + /// + /// True if the user can sort the column; false if the user cannot sort the column. The default is the current property value. + /// + public bool CanUserSort + { + get + { + if (this.CanUserSortInternal.HasValue) + { + return this.CanUserSortInternal.Value; + } +#if FEATURE_ICOLLECTIONVIEW_SORT + else if (this.OwningGrid != null) + { + string propertyPath = GetSortPropertyName(); + Type propertyType = this.OwningGrid.DataConnection.DataType.GetNestedPropertyType(propertyPath); + + // if the type is nullable, then we will compare the non-nullable type + if (TypeHelper.IsNullableType(propertyType)) + { + propertyType = TypeHelper.GetNonNullableType(propertyType); + } + + // return whether or not the property type can be compared + return typeof(IComparable).IsAssignableFrom(propertyType) ? true : false; + } +#endif + else + { + return DATAGRIDCOLUMN_defaultCanUserSort; + } + } + + set + { + this.CanUserSortInternal = value; + } + } + + /// + /// Gets or sets the style that is used when rendering cells in the column. + /// + /// + /// The style that is used when rendering cells in the column. The default is null. + /// + public Style CellStyle + { + get + { + return _cellStyle; + } + + set + { + if (_cellStyle != value) + { + Style previousStyle = _cellStyle; + _cellStyle = value; + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnCellStyleChanged(this, previousStyle); + } + } + } + } + + /// + /// Gets or sets the binding that will be used to get or set cell content for the clipboard. + /// + public virtual Binding ClipboardContentBinding + { + get + { + return _clipboardContentBinding; + } + + set + { + _clipboardContentBinding = value; + } + } + + /// + /// Gets or sets the display position of the column relative to the other columns in the . + /// + /// + /// The zero-based position of the column as it is displayed in the associated . The default is the index of the corresponding in the collection. + /// + /// + /// When setting this property, the specified value is less than -1 or equal to . + /// + /// -or- + /// + /// When setting this property on a column in a , the specified value is less than zero or greater than or equal to the number of columns in the . + /// + /// + /// When setting this property, the is already making adjustments. For example, this exception is thrown when you attempt to set in a event handler. + /// + /// -or- + /// + /// When setting this property, the specified value would result in a frozen column being displayed in the range of unfrozen columns, or an unfrozen column being displayed in the range of frozen columns. + /// + public int DisplayIndex + { + get + { + if (this.OwningGrid != null && this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + return _displayIndexWithFiller - 1; + } + else + { + return _displayIndexWithFiller; + } + } + + set + { + if (value == int.MaxValue) + { + throw DataGridError.DataGrid.ValueMustBeLessThan("value", "DisplayIndex", int.MaxValue); + } + + if (this.OwningGrid != null) + { + if (this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + value++; + } + + if (_displayIndexWithFiller != value) + { + if (value < 0 || value >= this.OwningGrid.ColumnsItemsInternal.Count) + { + throw DataGridError.DataGrid.ValueMustBeBetween("value", "DisplayIndex", 0, true, this.OwningGrid.Columns.Count, false); + } + + // Will throw an error if a visible frozen column is placed inside a non-frozen area or vice-versa. + this.OwningGrid.OnColumnDisplayIndexChanging(this, value); + _displayIndexWithFiller = value; + try + { + this.OwningGrid.InDisplayIndexAdjustments = true; + this.OwningGrid.OnColumnDisplayIndexChanged(this); + this.OwningGrid.OnColumnDisplayIndexChanged_PostNotification(); + } + finally + { + this.OwningGrid.InDisplayIndexAdjustments = false; + } + } + } + else + { + if (value < -1) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "DisplayIndex", -1); + } + + _displayIndexWithFiller = value; + } + } + } + + /// + /// Gets or sets the style for the drag indicator. + /// + public Style DragIndicatorStyle + { + get + { + return _dragIndicatorStyle; + } + + set + { + if (_dragIndicatorStyle != value) + { + _dragIndicatorStyle = value; + } + } + } + + /// + /// Gets or sets the style for the header. + /// + public Style HeaderStyle + { + get + { + return _headerStyle; + } + + set + { + if (_headerStyle != value) + { + Style previousStyle = _headerStyle; + _headerStyle = value; + if (_headerCell != null) + { + _headerCell.EnsureStyle(previousStyle); + } + } + } + } + + /// + /// Gets or sets the header object. + /// + public object Header + { + get + { + return _header; + } + + set + { + if (_header != value) + { + _header = value; + if (_headerCell != null) + { + _headerCell.Content = _header; + } + } + } + } + + /// + /// Gets a value indicating whether this column is autoGenerated. + /// + public bool IsAutoGenerated + { + get; + internal set; + } + + /// + /// Gets a value indicating whether this column is frozen. + /// + public bool IsFrozen + { + get; + internal set; + } + + /// + /// Gets or sets a value indicating whether this column is read-only. + /// + public bool IsReadOnly + { + get + { + if (this.OwningGrid == null) + { + return _isReadOnly ?? DATAGRIDCOLUMN_defaultIsReadOnly; + } + + if (_isReadOnly != null) + { + return _isReadOnly.Value || this.OwningGrid.IsReadOnly; + } + + return this.OwningGrid.GetColumnReadOnlyState(this, DATAGRIDCOLUMN_defaultIsReadOnly); + } + + set + { + if (value != _isReadOnly) + { + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnReadOnlyStateChanging(this, value); + } + + _isReadOnly = value; + } + } + } + + /// + /// Gets or sets the column's maximum width. + /// + public double MaxWidth + { + get + { + if (_maxWidth.HasValue) + { + return _maxWidth.Value; + } + + return double.PositiveInfinity; + } + + set + { + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", 0); + } + + if (value < this.ActualMinWidth) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MaxWidth", "MinWidth"); + } + + if (!_maxWidth.HasValue || _maxWidth.Value != value) + { + double oldValue = this.ActualMaxWidth; + _maxWidth = value; + if (this.OwningGrid != null && this.OwningGrid.ColumnsInternal != null) + { + this.OwningGrid.OnColumnMaxWidthChanged(this, oldValue); + } + } + } + } + + /// + /// Gets or sets the column's minimum width. + /// + public double MinWidth + { + get + { + if (_minWidth.HasValue) + { + return _minWidth.Value; + } + + return 0; + } + + set + { + if (double.IsNaN(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN("MinWidth"); + } + + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "MinWidth", 0); + } + + if (double.IsPositiveInfinity(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("MinWidth"); + } + + if (value > this.ActualMaxWidth) + { + throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo("value", "MinWidth", "MaxWidth"); + } + + if (!_minWidth.HasValue || _minWidth.Value != value) + { + double oldValue = this.ActualMinWidth; + _minWidth = value; + if (this.OwningGrid != null && this.OwningGrid.ColumnsInternal != null) + { + this.OwningGrid.OnColumnMinWidthChanged(this, oldValue); + } + } + } + } + + /// + /// Gets or sets the column's sort direction. Null indicates no sorting. + /// + public DataGridSortDirection? SortDirection + { + get + { + return _sortDirection; + } + + set + { + if (value != _sortDirection) + { + _sortDirection = value; + + if (this.HasHeaderCell) + { + this.HeaderCell.ApplyState(true /*useTransitions*/); + } + } + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Gets or sets the name of the member to use for sorting, if not using the default. + /// + public string SortMemberPath + { + get; + set; + } +#endif + + /// + /// Gets or sets an object associated with this column. + /// + public object Tag + { + get; + set; + } + + /// + /// Gets or sets the column's visibility. + /// + public Visibility Visibility + { + get + { + return _visibility; + } + + set + { + if (value != this.Visibility) + { + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnVisibleStateChanging(this); + } + + _visibility = value; + + if (_headerCell != null) + { + _headerCell.Visibility = _visibility; + } + + if (this.OwningGrid != null) + { + this.OwningGrid.OnColumnVisibleStateChanged(this); + } + } + } + } + + /// + /// Gets or sets the column's width. + /// + public DataGridLength Width + { + get + { + if (_width.HasValue) + { + return _width.Value; + } + else if (this.OwningGrid != null) + { + return this.OwningGrid.ColumnWidth; + } + else + { + return DataGridLength.Auto; + } + } + + set + { + if (!_width.HasValue || _width.Value != value) + { + if (!_settingWidthInternally) + { + this.InheritsWidth = false; + } + + if (this.OwningGrid != null) + { + bool isDesignMode = Windows.ApplicationModel.DesignMode.DesignModeEnabled; + DataGridLength width = CoerceWidth(value); + if (width.IsStar != this.Width.IsStar || isDesignMode) + { + // If a column has changed either from or to a star value, we want to recalculate all + // star column widths. They are recalculated during Measure based off what the value we set here. + SetWidthInternalNoCallback(width); + this.IsInitialDesiredWidthDetermined = false; + this.OwningGrid.OnColumnWidthChanged(this); + } + else + { + // If a column width's value is simply changing, we resize it (to the right only). + Resize(width.Value, width.UnitType, width.DesiredValue, width.DisplayValue, false); + } + } + else + { + SetWidthInternalNoCallback(value); + } + } + } + } + + internal bool ActualCanUserResize + { + get + { + if (this.OwningGrid == null || this.OwningGrid.CanUserResizeColumns == false || this is DataGridFillerColumn) + { + return false; + } + + return this.CanUserResizeInternal ?? true; + } + } + + // MaxWidth from local setting or DataGrid setting + internal double ActualMaxWidth + { + get + { + return _maxWidth ?? (this.OwningGrid != null ? this.OwningGrid.MaxColumnWidth : double.PositiveInfinity); + } + } + + // MinWidth from local setting or DataGrid setting + internal double ActualMinWidth + { + get + { + double minWidth = _minWidth ?? (this.OwningGrid != null ? this.OwningGrid.MinColumnWidth : 0); + if (this.Width.IsStar) + { + return Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, minWidth); + } + + return minWidth; + } + } + + internal List BindingPaths + { + get + { + if (_bindingPaths != null) + { + return _bindingPaths; + } + + _bindingPaths = CreateBindingPaths(); + return _bindingPaths; + } + } + + internal bool? CanUserReorderInternal + { + get; + set; + } + + internal bool? CanUserResizeInternal + { + get; + set; + } + + internal bool? CanUserSortInternal + { + get; + set; + } + + internal bool DisplayIndexHasChanged + { + get; + set; + } + + internal int DisplayIndexWithFiller + { + get + { + return _displayIndexWithFiller; + } + + set + { + Debug.Assert(value >= -1, "Expected value >= -1."); + Debug.Assert(value < int.MaxValue, "Expected value < int.MaxValue."); + + _displayIndexWithFiller = value; + } + } + + internal bool HasHeaderCell + { + get + { + return _headerCell != null; + } + } + + internal DataGridColumnHeader HeaderCell + { + get + { + if (_headerCell == null) + { + _headerCell = CreateHeader(); + } + + return _headerCell; + } + } + + internal int Index + { + get; + set; + } + + /// + /// Gets a value indicating whether or not this column inherits its Width value from the DataGrid. + /// + internal bool InheritsWidth + { + get; + private set; + } + + /// + /// Gets or sets a value indicating whether the column has been fully measured. When a column is initially added, + /// we won't know its initial desired value until all rows have been measured. + /// + internal bool IsInitialDesiredWidthDetermined + { + get; + set; + } + + internal bool IsVisible + { + get + { + return this.Visibility == Visibility.Visible; + } + } + + internal double LayoutRoundedWidth + { + get; + private set; + } + + internal DataGrid OwningGrid + { + get; + set; + } + + /// + /// Returns the column which contains the given element + /// + /// element contained in a column + /// Column that contains the element, or null if not found + public static DataGridColumn GetColumnContainingElement(FrameworkElement element) + { + // Walk up the tree to find the DataGridCell or DataGridColumnHeader that contains the element + DependencyObject parent = element; + while (parent != null) + { + DataGridCell cell = parent as DataGridCell; + if (cell != null) + { + return cell.OwningColumn; + } + + DataGridColumnHeader columnHeader = parent as DataGridColumnHeader; + if (columnHeader != null) + { + return columnHeader.OwningColumn; + } + + parent = VisualTreeHelper.GetParent(parent); + } + + return null; + } + + /// + /// Returns the column's content for the provided row. + /// + /// Row to get the content for. + /// The column's content for the provided row. + public FrameworkElement GetCellContent(DataGridRow dataGridRow) + { + if (dataGridRow == null) + { + throw new ArgumentNullException("dataGridRow"); + } + + if (this.OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(this.GetType()); + } + + if (dataGridRow.OwningGrid == this.OwningGrid) + { + Debug.Assert(this.Index >= 0, "Expected positive Index."); + Debug.Assert(this.Index < this.OwningGrid.ColumnsItemsInternal.Count, "Expected smaller Index."); + + DataGridCell dataGridCell = dataGridRow.Cells[this.Index]; + if (dataGridCell != null) + { + return dataGridCell.Content as FrameworkElement; + } + } + + return null; + } + + /// + /// Returns the column's content for the provided row dataItem. + /// + /// Row dataItem to get the content for. + /// The column's content for the provided row dataItem. + public FrameworkElement GetCellContent(object dataItem) + { + if (dataItem == null) + { + throw new ArgumentNullException("dataItem"); + } + + if (this.OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(this.GetType()); + } + + Debug.Assert(this.Index >= 0, "Expected positive Index."); + Debug.Assert(this.Index < this.OwningGrid.ColumnsItemsInternal.Count, "Expected smaller Index."); + + DataGridRow dataGridRow = this.OwningGrid.GetRowFromItem(dataItem); + if (dataGridRow == null) + { + return null; + } + + return GetCellContent(dataGridRow); + } + + /// + /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// The previous, unedited value in the cell being edited. + /// + protected virtual void CancelCellEdit(FrameworkElement editingElement, object uneditedValue) + { + } + + /// + /// When overridden in a derived class, gets an editing element that is bound to the column's property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new editing element that is bound to the column's property value. + /// + protected abstract FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem); + + /// + /// When overridden in a derived class, gets a read-only element that is bound to the column's + /// property value. + /// + /// + /// The cell that will contain the generated element. + /// + /// + /// The data item represented by the row that contains the intended cell. + /// + /// + /// A new, read-only element that is bound to the column's property value. + /// + protected abstract FrameworkElement GenerateElement(DataGridCell cell, object dataItem); + + /// + /// Called by a specific column type when one of its properties changed, + /// and its current cells need to be updated. + /// + /// Indicates which property changed and caused this call + protected void NotifyPropertyChanged(string propertyName) + { + if (this.OwningGrid == null) + { + return; + } + + this.OwningGrid.RefreshColumnElements(this, propertyName); + } + + /// + /// When overridden in a derived class, called when a cell in the column enters editing mode. + /// + /// + /// The element that the column displays for a cell in editing mode. + /// + /// + /// Information about the user gesture that is causing a cell to enter editing mode. + /// + /// + /// The unedited value. + /// + protected abstract object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs); + + /// + /// Called by the DataGrid control when a column asked for its + /// elements to be refreshed, typically because one of its properties changed. + /// + /// Indicates the element that needs to be refreshed + /// Indicates the computed row foreground based on RowForeground and AlternatingRowForeground + /// Indicates which property changed and caused this call + protected internal virtual void RefreshCellContent(FrameworkElement element, Brush computedRowForeground, string propertyName) + { + } + + /// + /// Called when the computed foreground of a row changed. + /// + protected internal virtual void RefreshForeground(FrameworkElement element, Brush computedRowForeground) + { + } + + internal void CancelCellEditInternal(FrameworkElement editingElement, object uneditedValue) + { + CancelCellEdit(editingElement, uneditedValue); + } + + /// + /// Coerces a DataGridLength to a valid value. If any value components are double.NaN, this method + /// coerces them to a proper initial value. For star columns, the desired width is calculated based + /// on the rest of the star columns. For pixel widths, the desired value is based on the pixel value. + /// For auto widths, the desired value is initialized as the column's minimum width. + /// + /// The DataGridLength to coerce. + /// The resultant (coerced) DataGridLength. + internal DataGridLength CoerceWidth(DataGridLength width) + { + double desiredValue = width.DesiredValue; + if (double.IsNaN(desiredValue)) + { + if (width.IsStar && this.OwningGrid != null && this.OwningGrid.ColumnsInternal != null) + { + double totalStarValues = 0; + double totalStarDesiredValues = 0; + double totalNonStarDisplayWidths = 0; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && c != this && !double.IsNaN(c.Width.DesiredValue))) + { + if (column.Width.IsStar) + { + totalStarValues += column.Width.Value; + totalStarDesiredValues += column.Width.DesiredValue; + } + else + { + totalNonStarDisplayWidths += column.ActualWidth; + } + } + + if (totalStarValues == 0) + { + // Compute the new star column's desired value based on the available space if there are no other visible star columns + desiredValue = Math.Max(this.ActualMinWidth, this.OwningGrid.CellsWidth - totalNonStarDisplayWidths); + } + else + { + // Otherwise, compute its desired value based on those of other visible star columns + desiredValue = totalStarDesiredValues * width.Value / totalStarValues; + } + } + else if (width.IsAbsolute) + { + desiredValue = width.Value; + } + else + { + desiredValue = this.ActualMinWidth; + } + } + + double displayValue = width.DisplayValue; + if (double.IsNaN(displayValue)) + { + displayValue = desiredValue; + } + + displayValue = Math.Max(this.ActualMinWidth, Math.Min(this.ActualMaxWidth, displayValue)); + + return new DataGridLength(width.Value, width.UnitType, desiredValue, displayValue); + } + + /// + /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to + /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal + /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some + /// pixel gaps and/or overlaps between columns. + /// + internal void ComputeLayoutRoundedWidth(double leftEdge) + { + if (this.OwningGrid != null && this.OwningGrid.UseLayoutRounding) + { + double scale; + if (TypeHelper.IsXamlRootAvailable && OwningGrid.XamlRoot != null) + { + scale = OwningGrid.XamlRoot.RasterizationScale; + } + else + { + scale = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel; + } + + double roundedLeftEdge = Math.Floor((scale * leftEdge) + 0.5) / scale; + double roundedRightEdge = Math.Floor((scale * (leftEdge + this.ActualWidth)) + 0.5) / scale; + this.LayoutRoundedWidth = roundedRightEdge - roundedLeftEdge; + } + else + { + this.LayoutRoundedWidth = this.ActualWidth; + } + } + + internal virtual List CreateBindingPaths() + { + List bindingPaths = new List(); + List bindings = null; + if (_inputBindings == null && this.OwningGrid != null) + { + DataGridRow row = this.OwningGrid.EditingRow; + if (row != null && row.Cells != null && row.Cells.Count > this.Index) + { + // Finds the input bindings if they don't already exist + this.GenerateEditingElementInternal(row.Cells[this.Index], row.DataContext); + } + } + + if (_inputBindings != null) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + // Use the editing bindings if they've already been created + bindings = _inputBindings; + } + + if (bindings != null) + { + // We're going to return the path of every active binding + foreach (BindingInfo binding in bindings) + { + if (binding != null && + binding.BindingExpression != null && + binding.BindingExpression.ParentBinding != null && + binding.BindingExpression.ParentBinding.Path != null) + { + bindingPaths.Add(binding.BindingExpression.ParentBinding.Path.Path); + } + } + } + + return bindingPaths; + } + + internal virtual List CreateBindings(FrameworkElement element, object dataItem, bool twoWay) + { + return element.GetBindingInfo(dataItem, twoWay, false /*useBlockList*/, true /*searchChildren*/, typeof(DataGrid)); + } + + internal virtual DataGridColumnHeader CreateHeader() + { + DataGridColumnHeader result = new DataGridColumnHeader(); + result.OwningColumn = this; + result.Content = _header; + result.EnsureStyle(null); + + return result; + } + + /// + /// Ensures that this column's width has been coerced to a valid value. + /// + internal void EnsureWidth() + { + SetWidthInternalNoCallback(CoerceWidth(this.Width)); + } + + internal FrameworkElement GenerateEditingElementInternal(DataGridCell cell, object dataItem) + { + if (_editingElement == null) + { + _editingElement = GenerateEditingElement(cell, dataItem); + } + + if (_inputBindings == null && _editingElement != null) + { + _inputBindings = CreateBindings(_editingElement, dataItem, true /*twoWay*/); + + // Setup all of the active input bindings to support validation + foreach (BindingInfo bindingData in _inputBindings) + { + if (bindingData.BindingExpression != null && + bindingData.BindingExpression.ParentBinding != null && + bindingData.BindingExpression.ParentBinding.UpdateSourceTrigger != UpdateSourceTrigger.Explicit) + { + Binding binding = new Binding(); + binding.Converter = bindingData.BindingExpression.ParentBinding.Converter; + binding.ConverterLanguage = bindingData.BindingExpression.ParentBinding.ConverterLanguage; + binding.ConverterParameter = bindingData.BindingExpression.ParentBinding.ConverterParameter; + binding.ElementName = bindingData.BindingExpression.ParentBinding.ElementName; + binding.FallbackValue = bindingData.BindingExpression.ParentBinding.FallbackValue; + binding.Mode = bindingData.BindingExpression.ParentBinding.Mode; + binding.Path = new PropertyPath(bindingData.BindingExpression.ParentBinding.Path.Path); + binding.RelativeSource = bindingData.BindingExpression.ParentBinding.RelativeSource; + binding.Source = bindingData.BindingExpression.ParentBinding.Source; + binding.TargetNullValue = bindingData.BindingExpression.ParentBinding.TargetNullValue; + binding.UpdateSourceTrigger = UpdateSourceTrigger.Explicit; + bindingData.Element.SetBinding(bindingData.BindingTarget, binding); + bindingData.BindingExpression = bindingData.Element.GetBindingExpression(bindingData.BindingTarget); + } + } + } + + return _editingElement; + } + + internal FrameworkElement GenerateElementInternal(DataGridCell cell, object dataItem) + { + return GenerateElement(cell, dataItem); + } + + /// + /// Gets the value of a cell according to the specified binding. + /// + /// The item associated with a cell. + /// The binding to get the value of. + /// The resultant cell value. + internal object GetCellValue(object item, Binding binding) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + object content = null; + if (binding != null) + { + this.OwningGrid.ClipboardContentControl.DataContext = item; + this.OwningGrid.ClipboardContentControl.SetBinding(ContentControl.ContentProperty, binding); + content = this.OwningGrid.ClipboardContentControl.GetValue(ContentControl.ContentProperty); + } + + return content; + } + + internal List GetInputBindings(FrameworkElement element, object dataItem) + { + if (_inputBindings != null) + { + return _inputBindings; + } + + return CreateBindings(element, dataItem, true /*twoWay*/); + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + /// + /// Gets the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description + /// describes an unchangeable sort that exists on the data. + /// + internal SortDescription? GetSortDescription() + { + if (this.OwningGrid != null + && this.OwningGrid.DataConnection != null + && this.OwningGrid.DataConnection.SortDescriptions != null) + { + string propertyName = GetSortPropertyName(); + + SortDescription sort = (new List(this.OwningGrid.DataConnection.SortDescriptions)) + .FirstOrDefault(s => s.PropertyName == propertyName); + + if (sort.PropertyName != null) + { + return sort; + } + + return null; + } + + return null; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_SORT + internal virtual string GetSortPropertyName() + { + return this.SortMemberPath; + } +#endif + + internal object PrepareCellForEditInternal(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + return PrepareCellForEdit(editingElement, editingEventArgs); + } + + /// + /// Clears the cached editing element. + /// + internal void RemoveEditingElement() + { + _editingElement = null; + _inputBindings = null; + _bindingPaths = null; + } + + /// + /// Attempts to resize the column's width to the desired DisplayValue, but limits the final size + /// to the column's minimum and maximum values. If star sizing is being used, then the column + /// can only decrease in size by the amount that the columns after it can increase in size. + /// Likewise, the column can only increase in size if other columns can spare the width. + /// + /// The new Value. + /// The new UnitType. + /// The new DesiredValue. + /// The new DisplayValue. + /// Whether or not this resize was initiated by a user action. + internal void Resize(double value, DataGridLengthUnitType unitType, double desiredValue, double displayValue, bool userInitiated) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + double newValue = value; + double newDesiredValue = desiredValue; + double newDisplayValue = Math.Max(this.ActualMinWidth, Math.Min(this.ActualMaxWidth, displayValue)); + DataGridLengthUnitType newUnitType = unitType; + + int starColumnsCount = 0; + double totalDisplayWidth = 0; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + column.EnsureWidth(); + totalDisplayWidth += column.ActualWidth; + starColumnsCount += (column != this && column.Width.IsStar) ? 1 : 0; + } + + bool hasInfiniteAvailableWidth = !this.OwningGrid.RowsPresenterAvailableSize.HasValue || double.IsPositiveInfinity(this.OwningGrid.RowsPresenterAvailableSize.Value.Width); + + // If we're using star sizing, we can only resize the column as much as the columns to the + // right will allow (i.e. until they hit their max or min widths). + if (!hasInfiniteAvailableWidth && (starColumnsCount > 0 || (unitType == DataGridLengthUnitType.Star && this.Width.IsStar && userInitiated))) + { + double limitedDisplayValue = this.Width.DisplayValue; + double availableIncrease = Math.Max(0, this.OwningGrid.CellsWidth - totalDisplayWidth); + double desiredChange = newDisplayValue - this.Width.DisplayValue; + if (desiredChange > availableIncrease) + { + // The desired change is greater than the amount of available space, + // so we need to decrease the widths of columns to the right to make room. + desiredChange -= availableIncrease; + double actualChange = desiredChange + this.OwningGrid.DecreaseColumnWidths(this.DisplayIndex + 1, -desiredChange, userInitiated); + limitedDisplayValue += availableIncrease + actualChange; + } + else if (desiredChange > 0) + { + // The desired change is positive but less than the amount of available space, + // so there's no need to decrease the widths of columns to the right. + limitedDisplayValue += desiredChange; + } + else + { + // The desired change is negative, so we need to increase the widths of columns to the right. + limitedDisplayValue += desiredChange + this.OwningGrid.IncreaseColumnWidths(this.DisplayIndex + 1, -desiredChange, userInitiated); + } + + if (this.ActualCanUserResize || (this.Width.IsStar && !userInitiated)) + { + newDisplayValue = limitedDisplayValue; + } + } + + if (userInitiated) + { + newDesiredValue = newDisplayValue; + if (!this.Width.IsStar) + { + this.InheritsWidth = false; + newValue = newDisplayValue; + newUnitType = DataGridLengthUnitType.Pixel; + } + else if (starColumnsCount > 0 && !hasInfiniteAvailableWidth) + { + // Recalculate star weight of this column based on the new desired value + this.InheritsWidth = false; + newValue = (this.Width.Value * newDisplayValue) / this.ActualWidth; + } + } + + DataGridLength oldWidth = this.Width; + SetWidthInternalNoCallback(new DataGridLength(Math.Min(double.MaxValue, newValue), newUnitType, newDesiredValue, newDisplayValue)); + if (this.Width != oldWidth) + { + this.OwningGrid.OnColumnWidthChanged(this); + } + } + + /// + /// Sets the column's Width to a new DataGridLength with a different DesiredValue. + /// + /// The new DesiredValue. + internal void SetWidthDesiredValue(double desiredValue) + { + SetWidthInternalNoCallback(new DataGridLength(this.Width.Value, this.Width.UnitType, desiredValue, this.Width.DisplayValue)); + } + + /// + /// Sets the column's Width to a new DataGridLength with a different DisplayValue. + /// + /// The new DisplayValue. + internal void SetWidthDisplayValue(double displayValue) + { + SetWidthInternalNoCallback(new DataGridLength(this.Width.Value, this.Width.UnitType, this.Width.DesiredValue, displayValue)); + } + + /// + /// Set the column's Width without breaking inheritance. + /// + /// The new Width. + internal void SetWidthInternal(DataGridLength width) + { + bool originalValue = _settingWidthInternally; + _settingWidthInternally = true; + try + { + this.Width = width; + } + finally + { + _settingWidthInternally = originalValue; + } + } + + /// + /// Sets the column's Width directly, without any callback effects. + /// + /// The new Width. + internal void SetWidthInternalNoCallback(DataGridLength width) + { + _width = width; + } + + /// + /// Set the column's star value. Whenever the star value changes, width inheritance is broken. + /// + /// The new star value. + internal void SetWidthStarValue(double value) + { + Debug.Assert(this.Width.IsStar, "Expected Width.IsStar."); + + this.InheritsWidth = false; + SetWidthInternalNoCallback(new DataGridLength(value, this.Width.UnitType, this.Width.DesiredValue, this.Width.DisplayValue)); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnCollection.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnCollection.cs new file mode 100644 index 0000000..6542818 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnCollection.cs @@ -0,0 +1,659 @@ +// 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; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + internal class DataGridColumnCollection : ObservableCollection + { + private DataGrid _owningGrid; + + public DataGridColumnCollection(DataGrid owningGrid) + { + _owningGrid = owningGrid; + this.ItemsInternal = new List(); + this.FillerColumn = new DataGridFillerColumn(owningGrid); + this.DisplayIndexMap = new List(); + this.RowGroupSpacerColumn = new DataGridFillerColumn(owningGrid); + } + + internal int AutogeneratedColumnCount + { + get; + set; + } + + internal List DisplayIndexMap + { + get; + set; + } + + internal DataGridFillerColumn FillerColumn + { + get; + private set; + } + + internal DataGridColumn FirstColumn + { + get + { + return GetFirstColumn(null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleNonFillerColumn + { + get + { + DataGridColumn dataGridColumn = this.FirstVisibleColumn; + if (dataGridColumn == this.RowGroupSpacerColumn) + { + dataGridColumn = GetNextVisibleColumn(dataGridColumn); + } + + return dataGridColumn; + } + } + + internal DataGridColumn FirstVisibleWritableColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + } + + internal DataGridColumn FirstVisibleScrollingColumn + { + get + { + return GetFirstColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal List ItemsInternal + { + get; + private set; + } + + internal DataGridColumn LastVisibleColumn + { + get + { + return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn LastVisibleScrollingColumn + { + get + { + return GetLastColumn(true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + } + + internal DataGridColumn LastVisibleWritableColumn + { + get + { + return GetLastColumn(true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + } + + internal DataGridFillerColumn RowGroupSpacerColumn + { + get; + private set; + } + + internal int VisibleColumnCount + { + get + { + int visibleColumnCount = 0; + for (int columnIndex = 0; columnIndex < this.ItemsInternal.Count; columnIndex++) + { + if (this.ItemsInternal[columnIndex].IsVisible) + { + visibleColumnCount++; + } + } + + return visibleColumnCount; + } + } + + internal double VisibleEdgedColumnsWidth + { + get; + private set; + } + + /// + /// Gets the number of star columns that are currently visible. + /// NOTE: Requires that EnsureVisibleEdgedColumnsWidth has been called. + /// + internal int VisibleStarColumnCount + { + get; + private set; + } + + protected override void ClearItems() + { + Debug.Assert(_owningGrid != null, "Expected non-null owning DataGrid."); + try + { + _owningGrid.NoCurrentCellChangeCount++; + if (this.ItemsInternal.Count > 0) + { + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + _owningGrid.OnClearingColumns(); + for (int columnIndex = 0; columnIndex < this.ItemsInternal.Count; columnIndex++) + { + // Detach the column... + this.ItemsInternal[columnIndex].OwningGrid = null; + } + + this.ItemsInternal.Clear(); + this.DisplayIndexMap.Clear(); + this.AutogeneratedColumnCount = 0; + _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/); + base.ClearItems(); + this.VisibleEdgedColumnsWidth = 0; + _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/); + } + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + + protected override void InsertItem(int columnIndex, DataGridColumn dataGridColumn) + { + Debug.Assert(_owningGrid != null, "Expected non-null owning DataGrid."); + try + { + _owningGrid.NoCurrentCellChangeCount++; + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + if (dataGridColumn == null) + { + throw new ArgumentNullException("dataGridColumn"); + } + + int columnIndexWithFiller = columnIndex; + if (dataGridColumn != this.RowGroupSpacerColumn && this.RowGroupSpacerColumn.IsRepresented) + { + columnIndexWithFiller++; + } + + // get the new current cell coordinates + DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnInsertingColumn(columnIndex, dataGridColumn); + + // insert the column into our internal list + this.ItemsInternal.Insert(columnIndexWithFiller, dataGridColumn); + dataGridColumn.Index = columnIndexWithFiller; + dataGridColumn.OwningGrid = _owningGrid; + dataGridColumn.RemoveEditingElement(); + if (dataGridColumn.IsVisible) + { + this.VisibleEdgedColumnsWidth += dataGridColumn.ActualWidth; + } + + // continue with the base insert + _owningGrid.OnInsertedColumn_PreNotification(dataGridColumn); + _owningGrid.OnColumnCollectionChanged_PreNotification(true /*columnsGrew*/); + + if (dataGridColumn != this.RowGroupSpacerColumn) + { + base.InsertItem(columnIndex, dataGridColumn); + } + + _owningGrid.OnInsertedColumn_PostNotification(newCurrentCellCoordinates, dataGridColumn.DisplayIndex); + _owningGrid.OnColumnCollectionChanged_PostNotification(true /*columnsGrew*/); + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + + protected override void RemoveItem(int columnIndex) + { + RemoveItemPrivate(columnIndex, false /*isSpacer*/); + } + + protected override void SetItem(int columnIndex, DataGridColumn dataGridColumn) + { + throw new NotSupportedException(); + } + + internal bool DisplayInOrder(int columnIndex1, int columnIndex2) + { + int displayIndex1 = ((DataGridColumn)this.ItemsInternal[columnIndex1]).DisplayIndexWithFiller; + int displayIndex2 = ((DataGridColumn)this.ItemsInternal[columnIndex2]).DisplayIndexWithFiller; + return displayIndex1 < displayIndex2; + } + + internal bool EnsureRowGrouping(bool rowGrouping) + { + // The insert below could cause the first column to be added. That causes a refresh + // which re-enters this method so instead of checking RowGroupSpacerColumn.IsRepresented, + // we need to check to see if it's actually in our collection instead. + bool spacerRepresented = (this.ItemsInternal.Count > 0) && (this.ItemsInternal[0] == this.RowGroupSpacerColumn); + if (rowGrouping && !spacerRepresented) + { + this.Insert(0, this.RowGroupSpacerColumn); + this.RowGroupSpacerColumn.IsRepresented = true; + return true; + } + else if (!rowGrouping && spacerRepresented) + { + Debug.Assert(this.ItemsInternal[0] == this.RowGroupSpacerColumn, "Unexpected RowGroupSpacerColumn value."); + + // We need to set IsRepresented to false before removing the RowGroupSpacerColumn + // otherwise, we'll remove the column after it + this.RowGroupSpacerColumn.IsRepresented = false; + RemoveItemPrivate(0, true /*isSpacer*/); + Debug.Assert(this.DisplayIndexMap.Count == this.ItemsInternal.Count, "Unexpected DisplayIndexMap.Count value."); + return true; + } + + return false; + } + + /// + /// In addition to ensuring that column widths are valid, this method updates the values of + /// VisibleEdgedColumnsWidth and VisibleStarColumnCount. + /// + internal void EnsureVisibleEdgedColumnsWidth() + { + this.VisibleStarColumnCount = 0; + this.VisibleEdgedColumnsWidth = 0; + for (int columnIndex = 0; columnIndex < this.ItemsInternal.Count; columnIndex++) + { + if (this.ItemsInternal[columnIndex].IsVisible) + { + this.ItemsInternal[columnIndex].EnsureWidth(); + if (this.ItemsInternal[columnIndex].Width.IsStar) + { + this.VisibleStarColumnCount++; + } + + this.VisibleEdgedColumnsWidth += this.ItemsInternal[columnIndex].ActualWidth; + } + } + } + + internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex) + { + if (displayIndex < 0 || displayIndex >= this.ItemsInternal.Count || displayIndex >= this.DisplayIndexMap.Count) + { + return null; + } + + int columnIndex = this.DisplayIndexMap[displayIndex]; + return this.ItemsInternal[columnIndex]; + } + + internal int GetColumnCount(bool isVisible, bool isFrozen, int fromColumnIndex, int toColumnIndex) + { + Debug.Assert(DisplayInOrder(fromColumnIndex, toColumnIndex), "Unexpected column display order."); + Debug.Assert(this.ItemsInternal[toColumnIndex].IsVisible == isVisible, "Unexpected column visibility state."); + Debug.Assert(this.ItemsInternal[toColumnIndex].IsFrozen == isFrozen, "Unexpected column frozen state."); + + int columnCount = 0; + DataGridColumn dataGridColumn = this.ItemsInternal[fromColumnIndex]; + + while (dataGridColumn != this.ItemsInternal[toColumnIndex]) + { + dataGridColumn = GetNextColumn(dataGridColumn, isVisible, isFrozen, null /*isReadOnly*/); + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + columnCount++; + } + + return columnCount; + } + + internal IEnumerable GetDisplayedColumns() + { + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + foreach (int columnIndex in this.DisplayIndexMap) + { + yield return this.ItemsInternal[columnIndex]; + } + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// + /// Criteria for inclusion. + /// Columns that meet the criteria, in ascending DisplayIndex order. + internal IEnumerable GetDisplayedColumns(Predicate filter) + { + Debug.Assert(filter != null, "Expected non-null filter."); + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + foreach (int columnIndex in this.DisplayIndexMap) + { + DataGridColumn column = this.ItemsInternal[columnIndex]; + if (filter(column)) + { + yield return column; + } + } + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// The columns are returned in the order specified by the reverse flag. + /// + /// Whether or not to return the columns in descending DisplayIndex order. + /// Criteria for inclusion. + /// Columns that meet the criteria, in the order specified by the reverse flag. + internal IEnumerable GetDisplayedColumns(bool reverse, Predicate filter) + { + return reverse ? GetDisplayedColumnsReverse(filter) : GetDisplayedColumns(filter); + } + + /// + /// Returns an enumeration of all columns that meet the criteria of the filter predicate. + /// The columns are returned in descending DisplayIndex order. + /// + /// Criteria for inclusion. + /// Columns that meet the criteria, in descending DisplayIndex order. + internal IEnumerable GetDisplayedColumnsReverse(Predicate filter) + { + Debug.Assert(filter != null, "Expected non-null filter."); + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + for (int displayIndex = this.DisplayIndexMap.Count - 1; displayIndex >= 0; displayIndex--) + { + DataGridColumn column = this.ItemsInternal[this.DisplayIndexMap[displayIndex]]; + if (filter(column)) + { + yield return column; + } + } + } + + internal DataGridColumn GetFirstColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + int index = 0; + while (index < this.DisplayIndexMap.Count) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || dataGridColumn.IsVisible == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + + index++; + } + + return null; + } + + internal DataGridColumn GetLastColumn(bool? isVisible, bool? isFrozen, bool? isReadOnly) + { + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + int index = this.DisplayIndexMap.Count - 1; + while (index >= 0) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || dataGridColumn.IsVisible == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + + index--; + } + + return null; + } + + internal DataGridColumn GetNextColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, null /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextColumn( + DataGridColumn dataGridColumnStart, + bool? isVisible, + bool? isFrozen, + bool? isReadOnly) + { + Debug.Assert(dataGridColumnStart != null, "Expected non-null dataGridColumnStart."); + Debug.Assert(this.ItemsInternal.Contains(dataGridColumnStart), "Expected dataGridColumnStart in ItemsInternal."); + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + + int index = dataGridColumnStart.DisplayIndexWithFiller + 1; + while (index < this.DisplayIndexMap.Count) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + + if ((isVisible == null || dataGridColumn.IsVisible == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + + index++; + } + + return null; + } + + internal DataGridColumn GetNextVisibleColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextVisibleFrozenColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, true /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetNextVisibleWritableColumn(DataGridColumn dataGridColumnStart) + { + return GetNextColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + + internal DataGridColumn GetPreviousColumn( + DataGridColumn dataGridColumnStart, + bool? isVisible, + bool? isFrozen, + bool? isReadOnly) + { + Debug.Assert(dataGridColumnStart != null, "Expected non-null dataGridColumnStart."); + Debug.Assert(this.ItemsInternal.Contains(dataGridColumnStart), "Expected dataGridColumnStart in ItemsInternal."); + Debug.Assert(this.ItemsInternal.Count == this.DisplayIndexMap.Count, "Unexpected DisplayIndexMap.Count value."); + + int index = dataGridColumnStart.DisplayIndexWithFiller - 1; + while (index >= 0) + { + DataGridColumn dataGridColumn = GetColumnAtDisplayIndex(index); + if ((isVisible == null || dataGridColumn.IsVisible == isVisible) && + (isFrozen == null || dataGridColumn.IsFrozen == isFrozen) && + (isReadOnly == null || dataGridColumn.IsReadOnly == isReadOnly)) + { + return dataGridColumn; + } + + index--; + } + + return null; + } + + internal DataGridColumn GetPreviousVisibleNonFillerColumn(DataGridColumn dataGridColumnStart) + { + DataGridColumn column = GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, null /*isReadOnly*/); + return (column is DataGridFillerColumn) ? null : column; + } + + internal DataGridColumn GetPreviousVisibleScrollingColumn(DataGridColumn dataGridColumnStart) + { + return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, false /*isFrozen*/, null /*isReadOnly*/); + } + + internal DataGridColumn GetPreviousVisibleWritableColumn(DataGridColumn dataGridColumnStart) + { + return GetPreviousColumn(dataGridColumnStart, true /*isVisible*/, null /*isFrozen*/, false /*isReadOnly*/); + } + + internal int GetVisibleColumnCount(int fromColumnIndex, int toColumnIndex) + { + Debug.Assert(DisplayInOrder(fromColumnIndex, toColumnIndex), "Unexpected column display order."); + Debug.Assert(this.ItemsInternal[toColumnIndex].IsVisible, "Unexpected column visibility state."); + + int columnCount = 0; + DataGridColumn dataGridColumn = this.ItemsInternal[fromColumnIndex]; + + while (dataGridColumn != this.ItemsInternal[toColumnIndex]) + { + dataGridColumn = GetNextVisibleColumn(dataGridColumn); + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + columnCount++; + } + + return columnCount; + } + + internal IEnumerable GetVisibleColumns() + { + Predicate filter = column => column.IsVisible; + return GetDisplayedColumns(filter); + } + + internal IEnumerable GetVisibleFrozenColumns() + { + Predicate filter = column => column.IsVisible && column.IsFrozen; + return GetDisplayedColumns(filter); + } + + internal double GetVisibleFrozenEdgedColumnsWidth() + { + double visibleFrozenColumnsWidth = 0; + for (int columnIndex = 0; columnIndex < this.ItemsInternal.Count; columnIndex++) + { + if (this.ItemsInternal[columnIndex].IsVisible && this.ItemsInternal[columnIndex].IsFrozen) + { + visibleFrozenColumnsWidth += this.ItemsInternal[columnIndex].ActualWidth; + } + } + + return visibleFrozenColumnsWidth; + } + + internal IEnumerable GetVisibleScrollingColumns() + { + Predicate filter = column => column.IsVisible && !column.IsFrozen; + return GetDisplayedColumns(filter); + } + + private void RemoveItemPrivate(int columnIndex, bool isSpacer) + { + Debug.Assert(_owningGrid != null, "Expected non-null owning DataGrid."); + try + { + _owningGrid.NoCurrentCellChangeCount++; + + if (_owningGrid.InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing the column collection while adjusting display indexes. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + int columnIndexWithFiller = columnIndex; + if (!isSpacer && this.RowGroupSpacerColumn.IsRepresented) + { + columnIndexWithFiller++; + } + + Debug.Assert(columnIndexWithFiller >= 0 && columnIndexWithFiller < this.ItemsInternal.Count, "Unexpected columnIndexWithFiller value."); + + DataGridColumn dataGridColumn = this.ItemsInternal[columnIndexWithFiller]; + DataGridCellCoordinates newCurrentCellCoordinates = _owningGrid.OnRemovingColumn(dataGridColumn); + this.ItemsInternal.RemoveAt(columnIndexWithFiller); + if (dataGridColumn.IsVisible) + { + this.VisibleEdgedColumnsWidth -= dataGridColumn.ActualWidth; + } + + dataGridColumn.OwningGrid = null; + dataGridColumn.RemoveEditingElement(); + + // continue with the base remove + _owningGrid.OnRemovedColumn_PreNotification(dataGridColumn); + _owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/); + if (!isSpacer) + { + base.RemoveItem(columnIndex); + } + + _owningGrid.OnRemovedColumn_PostNotification(newCurrentCellCoordinates); + _owningGrid.OnColumnCollectionChanged_PostNotification(false /*columnsGrew*/); + } + finally + { + _owningGrid.NoCurrentCellChangeCount--; + } + } + +#if DEBUG + internal bool Debug_VerifyColumnDisplayIndexes() + { + for (int columnDisplayIndex = 0; columnDisplayIndex < this.ItemsInternal.Count; columnDisplayIndex++) + { + if (GetColumnAtDisplayIndex(columnDisplayIndex) == null) + { + return false; + } + } + + return true; + } + + internal void Debug_PrintColumns() + { + foreach (DataGridColumn column in this.ItemsInternal) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} {1} {2}", column.Header, column.Index, column.DisplayIndex)); + } + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnEventArgs.cs new file mode 100644 index 0000000..52ec377 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnEventArgs.cs @@ -0,0 +1,37 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for column-related events. + /// + public class DataGridColumnEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The column that the event occurs for. + public DataGridColumnEventArgs(DataGridColumn column) + { + if (column == null) + { + throw new ArgumentNullException("column"); + } + + this.Column = column; + } + + /// + /// Gets the column that the event occurs for. + /// + public DataGridColumn Column + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeader.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeader.cs new file mode 100644 index 0000000..0f3aab7 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeader.cs @@ -0,0 +1,1118 @@ +// 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; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Devices.Input; +using Windows.Foundation; +using Windows.UI.Core; +using Windows.UI.Input; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Represents an individual column header. + /// + [TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StatePointerOver, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StatePressed, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = VisualStates.StateFocused, GroupName = VisualStates.GroupFocus)] + [TemplateVisualState(Name = VisualStates.StateUnfocused, GroupName = VisualStates.GroupFocus)] + [TemplateVisualState(Name = VisualStates.StateUnsorted, GroupName = VisualStates.GroupSort)] + [TemplateVisualState(Name = VisualStates.StateSortAscending, GroupName = VisualStates.GroupSort)] + [TemplateVisualState(Name = VisualStates.StateSortDescending, GroupName = VisualStates.GroupSort)] + public partial class DataGridColumnHeader : ContentControl + { + internal enum DragMode + { + None = 0, + PointerPressed = 1, + Drag = 2, + Resize = 3, + Reorder = 4 + } + + private const int DATAGRIDCOLUMNHEADER_dragThreshold = 2; + private const int DATAGRIDCOLUMNHEADER_resizeRegionWidthStrict = 5; + private const int DATAGRIDCOLUMNHEADER_resizeRegionWidthLoose = 9; + private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; + + private Visibility _desiredSeparatorVisibility; + + /// + /// Initializes a new instance of the class. + /// + public DataGridColumnHeader() + { + this.PointerCanceled += new PointerEventHandler(DataGridColumnHeader_PointerCanceled); + this.PointerCaptureLost += new PointerEventHandler(DataGridColumnHeader_PointerCaptureLost); + this.PointerPressed += new PointerEventHandler(DataGridColumnHeader_PointerPressed); + this.PointerReleased += new PointerEventHandler(DataGridColumnHeader_PointerReleased); + this.PointerMoved += new PointerEventHandler(DataGridColumnHeader_PointerMoved); + this.PointerEntered += new PointerEventHandler(DataGridColumnHeader_PointerEntered); + this.PointerExited += new PointerEventHandler(DataGridColumnHeader_PointerExited); + this.IsEnabledChanged += new DependencyPropertyChangedEventHandler(DataGridColumnHeader_IsEnabledChanged); + + DefaultStyleKey = typeof(DataGridColumnHeader); + } + + /// + /// Gets or sets the used to paint the column header separator lines. + /// + public Brush SeparatorBrush + { + get { return GetValue(SeparatorBrushProperty) as Brush; } + set { SetValue(SeparatorBrushProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SeparatorBrushProperty = + DependencyProperty.Register( + "SeparatorBrush", + typeof(Brush), + typeof(DataGridColumnHeader), + null); + + /// + /// Gets or sets a value indicating whether the column header separator lines are visible. + /// + public Visibility SeparatorVisibility + { + get { return (Visibility)GetValue(SeparatorVisibilityProperty); } + set { SetValue(SeparatorVisibilityProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SeparatorVisibilityProperty = + DependencyProperty.Register( + "SeparatorVisibility", + typeof(Visibility), + typeof(DataGridColumnHeader), + new PropertyMetadata(Visibility.Visible, OnSeparatorVisibilityPropertyChanged)); + + private static void OnSeparatorVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridColumnHeader columnHeader = d as DataGridColumnHeader; + + if (!columnHeader.IsHandlerSuspended(e.Property)) + { + columnHeader._desiredSeparatorVisibility = (Visibility)e.NewValue; + if (columnHeader.OwningGrid != null) + { + columnHeader.UpdateSeparatorVisibility(columnHeader.OwningGrid.ColumnsInternal.LastVisibleColumn); + } + else + { + columnHeader.UpdateSeparatorVisibility(null); + } + } + } + + internal int ColumnIndex + { + get + { + if (this.OwningColumn == null) + { + return -1; + } + + return this.OwningColumn.Index; + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + internal ListSortDirection? CurrentSortingState + { + get; + private set; + } +#endif + + internal DataGrid OwningGrid + { + get + { + if (this.OwningColumn != null && this.OwningColumn.OwningGrid != null) + { + return this.OwningColumn.OwningGrid; + } + + return null; + } + } + + internal DataGridColumn OwningColumn + { + get; + set; + } + + private bool HasFocus + { + get + { + return this.OwningGrid != null && + this.OwningColumn == this.OwningGrid.FocusedColumn && + this.OwningGrid.ColumnHeaderHasFocus; + } + } + + private bool IsPointerOver + { + get; + set; + } + + private bool IsPressed + { + get; + set; + } + + /// + /// Builds the visual tree for the column header when a new template is applied. + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + ApplyState(false /*useTransitions*/); + } + + /// + /// Called when the value of the property changes. + /// + /// The old value of the property. + /// The new value of the property. + /// + /// is not a UIElement. + /// + protected override void OnContentChanged(object oldContent, object newContent) + { + if (newContent is UIElement) + { + throw DataGridError.DataGridColumnHeader.ContentDoesNotSupportUIElements(); + } + + base.OnContentChanged(oldContent, newContent); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + if (this.OwningGrid != null && this.OwningColumn != this.OwningGrid.ColumnsInternal.FillerColumn) + { + return new DataGridColumnHeaderAutomationPeer(this); + } + + return base.OnCreateAutomationPeer(); + } + + internal void ApplyState(bool useTransitions) + { + DragMode dragMode = this.OwningGrid == null ? DragMode.None : this.OwningGrid.ColumnHeaderInteractionInfo.DragMode; + + // Common States + if (this.IsPressed && dragMode != DragMode.Resize) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StatePressed, VisualStates.StatePointerOver, VisualStates.StateNormal); + } + else if (this.IsPointerOver && dragMode != DragMode.Resize) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StatePointerOver, VisualStates.StateNormal); + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateNormal); + } + + // Focus States + if (this.HasFocus) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateFocused, VisualStates.StateRegular); + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateUnfocused); + } + + // Sort States + if (this.OwningColumn != null) + { + switch (this.OwningColumn.SortDirection) + { + case null: + VisualStates.GoToState(this, useTransitions, VisualStates.StateUnsorted); + break; + case DataGridSortDirection.Ascending: + VisualStates.GoToState(this, useTransitions, VisualStates.StateSortAscending, VisualStates.StateUnsorted); + break; + case DataGridSortDirection.Descending: + VisualStates.GoToState(this, useTransitions, VisualStates.StateSortDescending, VisualStates.StateUnsorted); + break; + } + } + } + + /// + /// Ensures that the correct Style is applied to this object. + /// + /// Caller's previous associated Style + internal void EnsureStyle(Style previousStyle) + { + if (this.Style != null && + this.Style != previousStyle && + (this.OwningColumn == null || this.Style != this.OwningColumn.HeaderStyle) && + (this.OwningGrid == null || this.Style != this.OwningGrid.ColumnHeaderStyle)) + { + return; + } + + Style style = null; + if (this.OwningColumn != null) + { + style = this.OwningColumn.HeaderStyle; + } + + if (style == null && this.OwningGrid != null) + { + style = this.OwningGrid.ColumnHeaderStyle; + } + + this.SetStyleWithType(style); + } + + internal void InvokeProcessSort() + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + if (this.OwningGrid.WaitForLostFocus(() => { this.InvokeProcessSort(); })) + { + return; + } + + if (this.OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditingMode*/)) + { + DispatcherQueue.TryEnqueue(System.DispatcherQueuePriority.Normal, () => { ProcessSort(); }); + } + } + + private void ProcessSort() + { + if (this.OwningColumn != null && + this.OwningGrid != null && + this.OwningGrid.EditingRow == null && + this.OwningColumn != this.OwningGrid.ColumnsInternal.FillerColumn && + this.OwningGrid.CanUserSortColumns && + this.OwningColumn.CanUserSort) + { + DataGridColumnEventArgs ea = new DataGridColumnEventArgs(this.OwningColumn); + this.OwningGrid.OnColumnSorting(ea); + +#if FEATURE_ICOLLECTIONVIEW_SORT + if (!ea.Handled && this.OwningGrid.DataConnection.AllowSort && this.OwningGrid.DataConnection.SortDescriptions != null) + { + // - DataConnection.AllowSort is true, and + // - SortDescriptionsCollection exists, and + // - the column's data type is comparable + DataGrid owningGrid = this.OwningGrid; + ListSortDirection newSortDirection; + SortDescription newSort; + + bool ctrl; + bool shift; + + KeyboardHelper.GetMetaKeyState(out ctrl, out shift); + + SortDescription? sort = this.OwningColumn.GetSortDescription(); + ICollectionView collectionView = owningGrid.DataConnection.CollectionView; + Debug.Assert(collectionView != null); + try + { + owningGrid.OnUserSorting(); + using (collectionView.DeferRefresh()) + { + // If shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand + if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0) + { + if (collectionView.CanGroup && collectionView.GroupDescriptions != null) + { + // Make sure we sort by the GroupDescriptions first + for (int i = 0; i < collectionView.GroupDescriptions.Count; i++) + { + PropertyGroupDescription groupDescription = collectionView.GroupDescriptions[i] as PropertyGroupDescription; + if (groupDescription != null && collectionView.SortDescriptions.Count <= i || collectionView.SortDescriptions[i].PropertyName != groupDescription.PropertyName) + { + collectionView.SortDescriptions.Insert(Math.Min(i, collectionView.SortDescriptions.Count), new SortDescription(groupDescription.PropertyName, ListSortDirection.Ascending)); + } + } + while (collectionView.SortDescriptions.Count > collectionView.GroupDescriptions.Count) + { + collectionView.SortDescriptions.RemoveAt(collectionView.GroupDescriptions.Count); + } + } + else if (!shift) + { + owningGrid.DataConnection.SortDescriptions.Clear(); + } + } + + if (sort.HasValue) + { + // swap direction + switch (sort.Value.Direction) + { + case ListSortDirection.Ascending: + newSortDirection = ListSortDirection.Descending; + break; + default: + newSortDirection = ListSortDirection.Ascending; + break; + } + + newSort = new SortDescription(sort.Value.PropertyName, newSortDirection); + + // changing direction should not affect sort order, so we replace this column's + // sort description instead of just adding it to the end of the collection + int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort.Value); + if (oldIndex >= 0) + { + owningGrid.DataConnection.SortDescriptions.Remove(sort.Value); + owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort); + } + else + { + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } + } + else + { + // start new sort + newSortDirection = ListSortDirection.Ascending; + + string propertyName = this.OwningColumn.GetSortPropertyName(); + + // no-opt if we couldn't find a property to sort on + if (string.IsNullOrEmpty(propertyName)) + { + return; + } + + newSort = new SortDescription(propertyName, newSortDirection); + + owningGrid.DataConnection.SortDescriptions.Add(newSort); + } + } + } + finally + { + owningGrid.OnUserSorted(); + } + + sortProcessed = true; + } +#endif + + // Send the Invoked event for the column header's automation peer. + DataGridAutomationPeer.RaiseAutomationInvokeEvent(this); + } + } + + internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) + { + Visibility newVisibility = _desiredSeparatorVisibility; + + // Collapse separator for the last column if there is no filler column + if (this.OwningColumn != null && + this.OwningGrid != null && + _desiredSeparatorVisibility == Visibility.Visible && + this.OwningColumn == lastVisibleColumn && + !this.OwningGrid.ColumnsInternal.FillerColumn.IsActive) + { + newVisibility = Visibility.Collapsed; + } + + // Update the public property if it has changed + if (this.SeparatorVisibility != newVisibility) + { + this.SetValueNoCallback(DataGridColumnHeader.SeparatorVisibilityProperty, newVisibility); + } + } + + /// + /// Determines whether a column can be resized by dragging the border of its header. If star sizing + /// is being used, there are special conditions that can prevent a column from being resized: + /// 1. The column is the last visible column. + /// 2. All columns are constrained by either their maximum or minimum values. + /// + /// Column to check. + /// Whether or not the column can be resized by dragging its header. + private static bool CanResizeColumn(DataGridColumn column) + { + if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && + (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) + { + return false; + } + + return column.ActualCanUserResize; + } + + private bool TrySetResizeColumn(uint pointerId, DataGridColumn column) + { + // If Datagrid.CanUserResizeColumns == false, then the column can still override it + if (this.OwningGrid != null && CanResizeColumn(column)) + { + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + Debug.Assert(interactionInfo.DragMode != DragMode.None, "Expected _dragMode other than None."); + + interactionInfo.DragColumn = column; + interactionInfo.DragMode = DragMode.Resize; + interactionInfo.DragPointerId = pointerId; + + return true; + } + + return false; + } + + private bool CanReorderColumn(DataGridColumn column) + { + return this.OwningGrid.CanUserReorderColumns && + !(column is DataGridFillerColumn) && + ((column.CanUserReorderInternal.HasValue && column.CanUserReorderInternal.Value) || !column.CanUserReorderInternal.HasValue); + } + + private void DataGridColumnHeader_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void DataGridColumnHeader_PointerCaptureLost(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void CancelPointer(PointerRoutedEventArgs e) + { + // When the user stops interacting with the column headers, the drag mode needs to be reset and any open popups closed. + if (this.OwningGrid != null) + { + this.IsPressed = false; + this.IsPointerOver = false; + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + bool setResizeCursor = false; + + if (this.OwningGrid.ColumnHeaders != null) + { + Point pointerPositionHeaders = e.GetCurrentPoint(this.OwningGrid.ColumnHeaders).Position; + setResizeCursor = interactionInfo.DragMode == DragMode.Resize && pointerPositionHeaders.X > 0 && pointerPositionHeaders.X < this.OwningGrid.ActualWidth; + } + + if (!setResizeCursor) + { + SetOriginalCursor(); + } + + if (interactionInfo.DragPointerId == e.Pointer.PointerId) + { + this.OwningGrid.ResetColumnHeaderInteractionInfo(); + } + + if (setResizeCursor) + { + SetResizeCursor(e.Pointer, e.GetCurrentPoint(this).Position); + } + + ApplyState(false /*useTransitions*/); + } + } + + private void DataGridColumnHeader_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (this.OwningGrid != null && !(bool)e.NewValue) + { + this.IsPressed = false; + this.IsPointerOver = false; + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (interactionInfo.CapturedPointer != null) + { + ReleasePointerCapture(interactionInfo.CapturedPointer); + } + + ApplyState(false /*useTransitions*/); + } + } + + private void DataGridColumnHeader_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (!this.IsEnabled || this.OwningGrid == null) + { + return; + } + + this.IsPointerOver = true; + + SetResizeCursor(e.Pointer, e.GetCurrentPoint(this).Position); + + ApplyState(true /*useTransitions*/); + } + + private void DataGridColumnHeader_PointerExited(object sender, PointerRoutedEventArgs e) + { + if (!this.IsEnabled || this.OwningGrid == null) + { + return; + } + + this.IsPointerOver = false; + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (interactionInfo.DragMode == DragMode.None && interactionInfo.ResizePointerId == e.Pointer.PointerId) + { + SetOriginalCursor(); + } + + ApplyState(true /*useTransitions*/); + } + + private void DataGridColumnHeader_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (this.OwningGrid == null || this.OwningColumn == null || e.Handled || !this.IsEnabled || this.OwningGrid.ColumnHeaderInteractionInfo.DragMode != DragMode.None) + { + return; + } + + var pointerPoint = e.GetCurrentPoint(this); + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse && !pointerPoint.Properties.IsLeftButtonPressed) + { + return; + } + + Debug.Assert(interactionInfo.DragPointerId == 0, "Expected _dragPointerId is 0."); + + bool handled = e.Handled; + + this.IsPressed = true; + + if (this.OwningGrid.ColumnHeaders != null) + { + Point pointerPosition = pointerPoint.Position; + + if (this.CapturePointer(e.Pointer)) + { + interactionInfo.CapturedPointer = e.Pointer; + } + else + { + interactionInfo.CapturedPointer = null; + } + + Debug.Assert(interactionInfo.DragMode == DragMode.None, "Expected _dragMode equals None."); + Debug.Assert(interactionInfo.DragColumn == null, "Expected _dragColumn is null."); + interactionInfo.DragMode = DragMode.PointerPressed; + interactionInfo.DragPointerId = e.Pointer.PointerId; + interactionInfo.FrozenColumnsWidth = this.OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); + interactionInfo.PressedPointerPositionHeaders = interactionInfo.LastPointerPositionHeaders = this.Translate(this.OwningGrid.ColumnHeaders, pointerPosition); + + double distanceFromLeft = pointerPosition.X; + double distanceFromRight = this.ActualWidth - distanceFromLeft; + DataGridColumn currentColumn = this.OwningColumn; + DataGridColumn previousColumn = null; + if (!(this.OwningColumn is DataGridFillerColumn)) + { + previousColumn = this.OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn); + } + + int resizeRegionWidth = e.Pointer.PointerDeviceType == PointerDeviceType.Touch ? DATAGRIDCOLUMNHEADER_resizeRegionWidthLoose : DATAGRIDCOLUMNHEADER_resizeRegionWidthStrict; + + if (distanceFromRight <= resizeRegionWidth) + { + handled = TrySetResizeColumn(e.Pointer.PointerId, currentColumn); + } + else if (distanceFromLeft <= resizeRegionWidth && previousColumn != null) + { + handled = TrySetResizeColumn(e.Pointer.PointerId, previousColumn); + } + + if (interactionInfo.DragMode == DragMode.Resize && interactionInfo.DragColumn != null) + { + interactionInfo.DragStart = interactionInfo.LastPointerPositionHeaders; + interactionInfo.OriginalWidth = interactionInfo.DragColumn.ActualWidth; + interactionInfo.OriginalHorizontalOffset = this.OwningGrid.HorizontalOffset; + + handled = true; + } + } + + e.Handled = handled; + + ApplyState(true /*useTransitions*/); + } + + private void DataGridColumnHeader_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (this.OwningGrid == null || this.OwningColumn == null || e.Handled || !this.IsEnabled) + { + return; + } + + var pointerPoint = e.GetCurrentPoint(this); + + if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse && pointerPoint.Properties.IsLeftButtonPressed) + { + return; + } + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (interactionInfo.DragPointerId != 0 && interactionInfo.DragPointerId != e.Pointer.PointerId) + { + return; + } + + Point pointerPosition = pointerPoint.Position; + Point pointerPositionHeaders = e.GetCurrentPoint(this.OwningGrid.ColumnHeaders).Position; + bool handled = e.Handled; + + this.IsPressed = false; + + if (this.OwningGrid.ColumnHeaders != null) + { + switch (interactionInfo.DragMode) + { + case DragMode.PointerPressed: + { + // Completed a click or tap without dragging, so raise the DataGrid.Sorting event. + InvokeProcessSort(); + break; + } + + case DragMode.Reorder: + { + // Find header hovered over + int targetIndex = this.GetReorderingTargetDisplayIndex(pointerPositionHeaders); + + if ((!this.OwningColumn.IsFrozen && targetIndex >= this.OwningGrid.FrozenColumnCount) || + (this.OwningColumn.IsFrozen && targetIndex < this.OwningGrid.FrozenColumnCount)) + { + this.OwningColumn.DisplayIndex = targetIndex; + + DataGridColumnEventArgs ea = new DataGridColumnEventArgs(this.OwningColumn); + this.OwningGrid.OnColumnReordered(ea); + } + + DragCompletedEventArgs dragCompletedEventArgs = new DragCompletedEventArgs(pointerPosition.X - interactionInfo.DragStart.Value.X, pointerPosition.Y - interactionInfo.DragStart.Value.Y, false); + this.OwningGrid.OnColumnHeaderDragCompleted(dragCompletedEventArgs); + break; + } + + case DragMode.Drag: + { + DragCompletedEventArgs dragCompletedEventArgs = new DragCompletedEventArgs(0, 0, false); + this.OwningGrid.OnColumnHeaderDragCompleted(dragCompletedEventArgs); + break; + } + } + + SetResizeCursor(e.Pointer, pointerPosition); + + // Variables that track drag mode states get reset in DataGridColumnHeader_LostPointerCapture + if (interactionInfo.CapturedPointer != null) + { + ReleasePointerCapture(interactionInfo.CapturedPointer); + } + + this.OwningGrid.ResetColumnHeaderInteractionInfo(); + handled = true; + } + + e.Handled = handled; + + ApplyState(true /*useTransitions*/); + } + + private void DataGridColumnHeader_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (this.OwningColumn == null || this.OwningGrid == null || this.OwningGrid.ColumnHeaders == null || !this.IsEnabled) + { + return; + } + + var pointerPoint = e.GetCurrentPoint(this); + Point pointerPosition = pointerPoint.Position; + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (pointerPoint.IsInContact && (interactionInfo.DragPointerId == 0 || interactionInfo.DragPointerId == e.Pointer.PointerId)) + { + Point pointerPositionHeaders = e.GetCurrentPoint(this.OwningGrid.ColumnHeaders).Position; + bool handled = false; + + Debug.Assert(this.OwningGrid.Parent is UIElement, "Expected owning DataGrid's parent to be a UIElement."); + + double distanceFromLeft = pointerPosition.X; + double distanceFromRight = this.ActualWidth - distanceFromLeft; + + OnPointerMove_Resize(ref handled, pointerPositionHeaders); + OnPointerMove_Reorder(ref handled, e.Pointer, pointerPosition, pointerPositionHeaders, distanceFromLeft, distanceFromRight); + + // If nothing was done about moving the pointer while the pointer is down, remember the dragging, but do not + // claim the event was actually handled. + if (interactionInfo.DragMode == DragMode.PointerPressed && + interactionInfo.PressedPointerPositionHeaders.HasValue && + Math.Abs(interactionInfo.PressedPointerPositionHeaders.Value.X - pointerPositionHeaders.X) + Math.Abs(interactionInfo.PressedPointerPositionHeaders.Value.Y - pointerPositionHeaders.Y) > DATAGRIDCOLUMNHEADER_dragThreshold) + { + interactionInfo.DragMode = DragMode.Drag; + interactionInfo.DragPointerId = e.Pointer.PointerId; + } + + if (interactionInfo.DragMode == DragMode.Drag) + { + DragDeltaEventArgs dragDeltaEventArgs = new DragDeltaEventArgs(pointerPositionHeaders.X - interactionInfo.LastPointerPositionHeaders.Value.X, pointerPositionHeaders.Y - interactionInfo.LastPointerPositionHeaders.Value.Y); + this.OwningGrid.OnColumnHeaderDragDelta(dragDeltaEventArgs); + } + + interactionInfo.LastPointerPositionHeaders = pointerPositionHeaders; + } + + SetResizeCursor(e.Pointer, pointerPosition); + + if (!this.IsPointerOver) + { + this.IsPointerOver = true; + ApplyState(true /*useTransitions*/); + } + } + + /// + /// Returns the column against whose top-left the reordering caret should be positioned + /// + /// Pointer position within the ColumnHeadersPresenter + /// Whether or not to scroll horizontally when a column is dragged out of bounds + /// If scroll is true, returns the horizontal amount that was scrolled + /// The column against whose top-left the reordering caret should be positioned. + private DataGridColumn GetReorderingTargetColumn(Point pointerPositionHeaders, bool scroll, out double scrollAmount) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + scrollAmount = 0; + double leftEdge = 0; + + if (this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.IsRepresented) + { + leftEdge = this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.ActualWidth; + } + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + double rightEdge = this.OwningGrid.CellsWidth; + if (this.OwningColumn.IsFrozen) + { + rightEdge = Math.Min(rightEdge, interactionInfo.FrozenColumnsWidth); + } + else if (this.OwningGrid.FrozenColumnCount > 0) + { + leftEdge = interactionInfo.FrozenColumnsWidth; + } + + if (pointerPositionHeaders.X < leftEdge) + { + if (scroll && + this.OwningGrid.HorizontalScrollBar != null && + this.OwningGrid.HorizontalScrollBar.Visibility == Visibility.Visible && + this.OwningGrid.HorizontalScrollBar.Value > 0) + { + double newVal = pointerPositionHeaders.X - leftEdge; + scrollAmount = Math.Min(newVal, this.OwningGrid.HorizontalScrollBar.Value); + this.OwningGrid.UpdateHorizontalOffset(scrollAmount + this.OwningGrid.HorizontalScrollBar.Value); + } + + pointerPositionHeaders.X = leftEdge; + } + else if (pointerPositionHeaders.X >= rightEdge) + { + if (scroll && + this.OwningGrid.HorizontalScrollBar != null && + this.OwningGrid.HorizontalScrollBar.Visibility == Visibility.Visible && + this.OwningGrid.HorizontalScrollBar.Value < this.OwningGrid.HorizontalScrollBar.Maximum) + { + double newVal = pointerPositionHeaders.X - rightEdge; + scrollAmount = Math.Min(newVal, this.OwningGrid.HorizontalScrollBar.Maximum - this.OwningGrid.HorizontalScrollBar.Value); + this.OwningGrid.UpdateHorizontalOffset(scrollAmount + this.OwningGrid.HorizontalScrollBar.Value); + } + + pointerPositionHeaders.X = rightEdge - 1; + } + + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetDisplayedColumns()) + { + Point pointerPosition = this.OwningGrid.ColumnHeaders.Translate(column.HeaderCell, pointerPositionHeaders); + double columnMiddle = column.HeaderCell.ActualWidth / 2; + if (pointerPosition.X >= 0 && pointerPosition.X <= columnMiddle) + { + return column; + } + else if (pointerPosition.X > columnMiddle && pointerPosition.X < column.HeaderCell.ActualWidth) + { + return this.OwningGrid.ColumnsInternal.GetNextVisibleColumn(column); + } + } + + return null; + } + + /// + /// Returns the display index to set the column to + /// + /// Pointer position relative to the column headers presenter + /// The display index to set the column to. + private int GetReorderingTargetDisplayIndex(Point pointerPositionHeaders) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + DataGridColumn targetColumn = GetReorderingTargetColumn(pointerPositionHeaders, false /*scroll*/, out _); + if (targetColumn != null) + { + return targetColumn.DisplayIndex > this.OwningColumn.DisplayIndex ? targetColumn.DisplayIndex - 1 : targetColumn.DisplayIndex; + } + else + { + return this.OwningGrid.Columns.Count - 1; + } + } + + private void OnPointerMove_BeginReorder(uint pointerId, Point pointerPosition) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + DataGridColumnHeader dragIndicator = new DataGridColumnHeader(); + dragIndicator.OwningColumn = this.OwningColumn; + dragIndicator.IsEnabled = false; + dragIndicator.Content = this.Content; + dragIndicator.ContentTemplate = this.ContentTemplate; + + Control dropLocationIndicator = new ContentControl(); + dropLocationIndicator.SetStyleWithType(this.OwningGrid.DropLocationIndicatorStyle); + + if (this.OwningColumn.DragIndicatorStyle != null) + { + dragIndicator.SetStyleWithType(this.OwningColumn.DragIndicatorStyle); + } + else if (this.OwningGrid.DragIndicatorStyle != null) + { + dragIndicator.SetStyleWithType(this.OwningGrid.DragIndicatorStyle); + } + + // If the user didn't style the dragIndicator's Width, default it to the column header's width. + if (double.IsNaN(dragIndicator.Width)) + { + dragIndicator.Width = this.ActualWidth; + } + + // If the user didn't style the dropLocationIndicator's Height, default to the column header's height. + if (double.IsNaN(dropLocationIndicator.Height)) + { + dropLocationIndicator.Height = this.ActualHeight; + } + + // pass the caret's data template to the user for modification. + DataGridColumnReorderingEventArgs columnReorderingEventArgs = new DataGridColumnReorderingEventArgs(this.OwningColumn) + { + DropLocationIndicator = dropLocationIndicator, + DragIndicator = dragIndicator + }; + this.OwningGrid.OnColumnReordering(columnReorderingEventArgs); + if (columnReorderingEventArgs.Cancel) + { + return; + } + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + // The app didn't cancel, so prepare for the reorder. + interactionInfo.DragColumn = this.OwningColumn; + Debug.Assert(interactionInfo.DragMode != DragMode.None, "Expected _dragMode other than None."); + interactionInfo.DragMode = DragMode.Reorder; + interactionInfo.DragPointerId = pointerId; + interactionInfo.DragStart = pointerPosition; + + // Display the reordering thumb. + this.OwningGrid.ColumnHeaders.DragColumn = this.OwningColumn; + this.OwningGrid.ColumnHeaders.DragIndicator = columnReorderingEventArgs.DragIndicator; + this.OwningGrid.ColumnHeaders.DropLocationIndicator = columnReorderingEventArgs.DropLocationIndicator; + } + + private void OnPointerMove_Reorder(ref bool handled, Pointer pointer, Point pointerPosition, Point pointerPositionHeaders, double distanceFromLeft, double distanceFromRight) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + if (handled) + { + return; + } + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + int resizeRegionWidth = pointer.PointerDeviceType == PointerDeviceType.Touch ? DATAGRIDCOLUMNHEADER_resizeRegionWidthLoose : DATAGRIDCOLUMNHEADER_resizeRegionWidthStrict; + + // Handle entry into reorder mode + if (interactionInfo.DragMode == DragMode.PointerPressed && + interactionInfo.DragColumn == null && + distanceFromRight > resizeRegionWidth && + distanceFromLeft > resizeRegionWidth && + interactionInfo.PressedPointerPositionHeaders.HasValue && + Math.Abs(interactionInfo.PressedPointerPositionHeaders.Value.X - pointerPositionHeaders.X) + Math.Abs(interactionInfo.PressedPointerPositionHeaders.Value.Y - pointerPositionHeaders.Y) > DATAGRIDCOLUMNHEADER_dragThreshold) + { + DragStartedEventArgs dragStartedEventArgs = + new DragStartedEventArgs(pointerPositionHeaders.X - interactionInfo.LastPointerPositionHeaders.Value.X, pointerPositionHeaders.Y - interactionInfo.LastPointerPositionHeaders.Value.Y); + this.OwningGrid.OnColumnHeaderDragStarted(dragStartedEventArgs); + + handled = CanReorderColumn(this.OwningColumn); + + if (handled) + { + OnPointerMove_BeginReorder(pointer.PointerId, pointerPosition); + } + } + + // Handle reorder mode (eg, positioning of the popup) + if (interactionInfo.DragMode == DragMode.Reorder && this.OwningGrid.ColumnHeaders.DragIndicator != null) + { + DragDeltaEventArgs dragDeltaEventArgs = new DragDeltaEventArgs(pointerPositionHeaders.X - interactionInfo.LastPointerPositionHeaders.Value.X, pointerPositionHeaders.Y - interactionInfo.LastPointerPositionHeaders.Value.Y); + this.OwningGrid.OnColumnHeaderDragDelta(dragDeltaEventArgs); + + // Find header we're hovering over + DataGridColumn targetColumn = GetReorderingTargetColumn(pointerPositionHeaders, !this.OwningColumn.IsFrozen /*scroll*/, out var scrollAmount); + + this.OwningGrid.ColumnHeaders.DragIndicatorOffset = pointerPosition.X - interactionInfo.DragStart.Value.X + scrollAmount; + this.OwningGrid.ColumnHeaders.InvalidateArrange(); + + if (this.OwningGrid.ColumnHeaders.DropLocationIndicator != null) + { + Point targetPosition = new Point(0, 0); + if (targetColumn == null || targetColumn == this.OwningGrid.ColumnsInternal.FillerColumn || targetColumn.IsFrozen != this.OwningColumn.IsFrozen) + { + targetColumn = this.OwningGrid.ColumnsInternal.GetLastColumn(true /*isVisible*/, this.OwningColumn.IsFrozen /*isFrozen*/, null /*isReadOnly*/); + targetPosition = targetColumn.HeaderCell.Translate(this.OwningGrid.ColumnHeaders, targetPosition); + targetPosition.X += targetColumn.ActualWidth; + } + else + { + targetPosition = targetColumn.HeaderCell.Translate(this.OwningGrid.ColumnHeaders, targetPosition); + } + + this.OwningGrid.ColumnHeaders.DropLocationIndicatorOffset = targetPosition.X - scrollAmount; + } + + handled = true; + } + } + + private void OnPointerMove_Resize(ref bool handled, Point pointerPositionHeaders) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (!handled && interactionInfo.DragMode == DragMode.Resize && interactionInfo.DragColumn != null && interactionInfo.DragStart.HasValue) + { + Debug.Assert(interactionInfo.ResizePointerId != 0, "Expected interactionInfo.ResizePointerId other than 0."); + + // Resize column + double pointerDelta = pointerPositionHeaders.X - interactionInfo.DragStart.Value.X; + double desiredWidth = interactionInfo.OriginalWidth + pointerDelta; + + desiredWidth = Math.Max(interactionInfo.DragColumn.ActualMinWidth, Math.Min(interactionInfo.DragColumn.ActualMaxWidth, desiredWidth)); + interactionInfo.DragColumn.Resize(interactionInfo.DragColumn.Width.Value, interactionInfo.DragColumn.Width.UnitType, interactionInfo.DragColumn.Width.DesiredValue, desiredWidth, true); + + this.OwningGrid.UpdateHorizontalOffset(interactionInfo.OriginalHorizontalOffset); + + handled = true; + } + } + + private void SetOriginalCursor() + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (interactionInfo.ResizePointerId != 0) + { + Debug.Assert(interactionInfo.OriginalCursor != null, "Expected non-null interactionInfo.OriginalCursor."); + + CoreWindow.GetForCurrentThread().PointerCursor = interactionInfo.OriginalCursor; + + interactionInfo.ResizePointerId = 0; + } + } + + private void SetResizeCursor(Pointer pointer, Point pointerPosition) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + + DataGridColumnHeaderInteractionInfo interactionInfo = this.OwningGrid.ColumnHeaderInteractionInfo; + + if (interactionInfo.DragMode != DragMode.None || this.OwningGrid == null || this.OwningColumn == null) + { + return; + } + + // Set mouse cursor if the column can be resized. + double distanceFromLeft = pointerPosition.X; + double distanceFromTop = pointerPosition.Y; + double distanceFromRight = this.ActualWidth - distanceFromLeft; + DataGridColumn currentColumn = this.OwningColumn; + DataGridColumn previousColumn = null; + + if (!(this.OwningColumn is DataGridFillerColumn)) + { + previousColumn = this.OwningGrid.ColumnsInternal.GetPreviousVisibleNonFillerColumn(currentColumn); + } + + int resizeRegionWidth = pointer.PointerDeviceType == PointerDeviceType.Touch ? DATAGRIDCOLUMNHEADER_resizeRegionWidthLoose : DATAGRIDCOLUMNHEADER_resizeRegionWidthStrict; + bool nearCurrentResizableColumnRightEdge = distanceFromRight <= resizeRegionWidth && currentColumn != null && CanResizeColumn(currentColumn) && distanceFromTop < this.ActualHeight; + bool nearPreviousResizableColumnLeftEdge = distanceFromLeft <= resizeRegionWidth && previousColumn != null && CanResizeColumn(previousColumn) && distanceFromTop < this.ActualHeight; + + if (this.OwningGrid.IsEnabled && (nearCurrentResizableColumnRightEdge || nearPreviousResizableColumnLeftEdge)) + { + CoreCursor currentCursor = CoreWindow.GetForCurrentThread().PointerCursor; + if (currentCursor != null && currentCursor.Type != CoreCursorType.SizeWestEast) + { + interactionInfo.OriginalCursor = currentCursor; + interactionInfo.ResizePointerId = pointer.PointerId; + CoreWindow.GetForCurrentThread().PointerCursor = new CoreCursor(CoreCursorType.SizeWestEast, 0); + } + } + else if (interactionInfo.ResizePointerId == pointer.PointerId) + { + SetOriginalCursor(); + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeaderInteractionInfo.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeaderInteractionInfo.cs new file mode 100644 index 0000000..ef3578d --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeaderInteractionInfo.cs @@ -0,0 +1,93 @@ +// 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 Windows.Foundation; +using Windows.UI.Core; +using Windows.UI.Xaml.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + internal class DataGridColumnHeaderInteractionInfo + { + internal Pointer CapturedPointer + { + get; + set; + } + + internal DataGridColumn DragColumn + { + get; + set; + } + + internal DataGridColumnHeader.DragMode DragMode + { + get; + set; + } + + internal uint DragPointerId + { + get; + set; + } + + internal Point? DragStart + { + get; + set; + } + + internal double FrozenColumnsWidth + { + get; + set; + } + + internal bool HasUserInteraction + { + get + { + return this.DragMode != DataGridColumnHeader.DragMode.None; + } + } + + internal Point? LastPointerPositionHeaders + { + get; + set; + } + + internal CoreCursor OriginalCursor + { + get; + set; + } + + internal double OriginalHorizontalOffset + { + get; + set; + } + + internal double OriginalWidth + { + get; + set; + } + + internal Point? PressedPointerPositionHeaders + { + get; + set; + } + + internal uint ResizePointerId + { + get; + set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeadersPresenter.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeadersPresenter.cs new file mode 100644 index 0000000..7f0d0ea --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnHeadersPresenter.cs @@ -0,0 +1,419 @@ +// 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; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Used within the template of a to specify the + /// location in the control's visual tree where the column headers are to be added. + /// + public sealed class DataGridColumnHeadersPresenter : Panel + { + private Control _dragIndicator; + private Control _dropLocationIndicator; + + /// + /// Gets or sets which column is currently being dragged. + /// + internal DataGridColumn DragColumn + { + get; + set; + } + + /// + /// Gets or sets the current drag indicator control. This value is null if no column is being dragged. + /// + internal Control DragIndicator + { + get + { + return _dragIndicator; + } + + set + { + if (value != _dragIndicator) + { + if (this.Children.Contains(_dragIndicator)) + { + this.Children.Remove(_dragIndicator); + } + + _dragIndicator = value; + if (_dragIndicator != null) + { + this.Children.Add(_dragIndicator); + } + } + } + } + + /// + /// Gets or sets the distance, in pixels, that the DragIndicator should be positioned away from the corresponding DragColumn. + /// + internal double DragIndicatorOffset + { + get; + set; + } + + /// + /// Gets or sets the drop location indicator control. This value is null if no column is being dragged. + /// + internal Control DropLocationIndicator + { + get + { + return _dropLocationIndicator; + } + + set + { + if (value != _dropLocationIndicator) + { + if (this.Children.Contains(_dropLocationIndicator)) + { + this.Children.Remove(_dropLocationIndicator); + } + + _dropLocationIndicator = value; + if (_dropLocationIndicator != null) + { + this.Children.Add(_dropLocationIndicator); + } + } + } + } + + /// + /// Gets or sets the distance, in pixels, that the drop location indicator should be positioned away from the left edge + /// of the ColumnsHeaderPresenter. + /// + internal double DropLocationIndicatorOffset + { + get; + set; + } + + internal DataGrid OwningGrid + { + get; + set; + } + + /// + /// Arranges the content of the . + /// + /// + /// The actual size used by the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + if (this.OwningGrid.AutoSizingColumns) + { + // When we initially load an auto-column, we have to wait for all the rows to be measured + // before we know its final desired size. We need to trigger a new round of measures now + // that the final sizes have been calculated. + this.OwningGrid.AutoSizingColumns = false; + return base.ArrangeOverride(finalSize); + } + + double dragIndicatorLeftEdge = 0; + double frozenLeftEdge = 0; + double scrollingLeftEdge = -this.OwningGrid.HorizontalOffset; + foreach (DataGridColumn dataGridColumn in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + DataGridColumnHeader columnHeader = dataGridColumn.HeaderCell; + Debug.Assert(columnHeader.OwningColumn == dataGridColumn, "Expected columnHeader owned by dataGridColumn."); + + if (dataGridColumn.IsFrozen) + { + columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); + columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform + if (this.DragColumn == dataGridColumn && this.DragIndicator != null) + { + dragIndicatorLeftEdge = frozenLeftEdge + this.DragIndicatorOffset; + } + + frozenLeftEdge += dataGridColumn.ActualWidth; + } + else + { + columnHeader.Arrange(new Rect(scrollingLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); + EnsureColumnHeaderClip(columnHeader, dataGridColumn.ActualWidth, finalSize.Height, frozenLeftEdge, scrollingLeftEdge); + if (this.DragColumn == dataGridColumn && this.DragIndicator != null) + { + dragIndicatorLeftEdge = scrollingLeftEdge + this.DragIndicatorOffset; + } + } + + scrollingLeftEdge += dataGridColumn.ActualWidth; + } + + if (this.DragColumn != null) + { + if (this.DragIndicator != null) + { + this.EnsureColumnReorderingClip(this.DragIndicator, finalSize.Height, frozenLeftEdge, dragIndicatorLeftEdge); + this.DragIndicator.Arrange(new Rect(dragIndicatorLeftEdge, 0, this.DragIndicator.ActualWidth, this.DragIndicator.ActualHeight)); + } + + if (this.DropLocationIndicator != null) + { + this.EnsureColumnReorderingClip(this.DropLocationIndicator, finalSize.Height, frozenLeftEdge, this.DropLocationIndicatorOffset); + this.DropLocationIndicator.Arrange(new Rect(this.DropLocationIndicatorOffset, 0, this.DropLocationIndicator.ActualWidth, this.DropLocationIndicator.ActualHeight)); + } + } + + // Arrange filler + this.OwningGrid.OnFillerColumnWidthNeeded(finalSize.Width); + DataGridFillerColumn fillerColumn = this.OwningGrid.ColumnsInternal.FillerColumn; + if (fillerColumn.FillerWidth > 0) + { + fillerColumn.HeaderCell.Visibility = Visibility.Visible; + fillerColumn.HeaderCell.Arrange(new Rect(scrollingLeftEdge, 0, fillerColumn.FillerWidth, finalSize.Height)); + } + else + { + fillerColumn.HeaderCell.Visibility = Visibility.Collapsed; + } + + // This needs to be updated after the filler column is configured + DataGridColumn lastVisibleColumn = this.OwningGrid.ColumnsInternal.LastVisibleColumn; + if (lastVisibleColumn != null) + { + lastVisibleColumn.HeaderCell.UpdateSeparatorVisibility(lastVisibleColumn); + } + + return finalSize; + } + + private static void EnsureColumnHeaderClip(DataGridColumnHeader columnHeader, double width, double height, double frozenLeftEdge, double columnHeaderLeftEdge) + { + // Clip the cell only if it's scrolled under frozen columns. Unfortunately, we need to clip in this case + // because cells could be transparent + if (frozenLeftEdge > columnHeaderLeftEdge) + { + RectangleGeometry rg = new RectangleGeometry(); + double xClip = Math.Min(width, frozenLeftEdge - columnHeaderLeftEdge); + rg.Rect = new Rect(xClip, 0, width - xClip, height); + columnHeader.Clip = rg; + } + else + { + columnHeader.Clip = null; + } + } + + /// + /// Clips the DragIndicator and DropLocationIndicator controls according to current ColumnHeaderPresenter constraints. + /// + /// The DragIndicator or DropLocationIndicator + /// The available height + /// The width of the frozen column region + /// The left edge of the control to clip + private void EnsureColumnReorderingClip(Control control, double height, double frozenColumnsWidth, double controlLeftEdge) + { + double leftEdge = 0; + double rightEdge = this.OwningGrid.CellsWidth; + double width = control.ActualWidth; + if (this.DragColumn.IsFrozen) + { + // If we're dragging a frozen column, we want to clip the corresponding DragIndicator control when it goes + // into the scrolling columns region, but not the DropLocationIndicator. + if (control == this.DragIndicator) + { + rightEdge = Math.Min(rightEdge, frozenColumnsWidth); + } + } + else if (this.OwningGrid.FrozenColumnCount > 0) + { + // If we're dragging a scrolling column, we want to clip both the DragIndicator and the DropLocationIndicator + // controls when they go into the frozen column range. + leftEdge = frozenColumnsWidth; + } + + RectangleGeometry rg = null; + if (leftEdge > controlLeftEdge) + { + rg = new RectangleGeometry(); + double xClip = Math.Min(width, leftEdge - controlLeftEdge); + rg.Rect = new Rect(xClip, 0, width - xClip, height); + } + + if (controlLeftEdge + width >= rightEdge) + { + if (rg == null) + { + rg = new RectangleGeometry(); + } + + rg.Rect = new Rect(rg.Rect.X, rg.Rect.Y, Math.Max(0, rightEdge - controlLeftEdge - rg.Rect.X), height); + } + + control.Clip = rg; + } + + /// + /// Measures the children of a to + /// prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (this.OwningGrid == null) + { + return base.MeasureOverride(availableSize); + } + + if (!this.OwningGrid.AreColumnHeadersVisible) + { + return new Size(0.0, 0.0); + } + + double height = this.OwningGrid.ColumnHeaderHeight; + bool autoSizeHeight; + if (double.IsNaN(height)) + { + // No explicit height values were set so we can autosize + height = 0; + autoSizeHeight = true; + } + else + { + autoSizeHeight = false; + } + + double totalDisplayWidth = 0; + this.OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + DataGridColumn lastVisibleColumn = this.OwningGrid.ColumnsInternal.LastVisibleColumn; + foreach (DataGridColumn column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + // Measure each column header + bool autoGrowWidth = column.Width.IsAuto || column.Width.IsSizeToHeader; + DataGridColumnHeader columnHeader = column.HeaderCell; + if (column != lastVisibleColumn) + { + columnHeader.UpdateSeparatorVisibility(lastVisibleColumn); + } + + // If we're not using star sizing or the current column can't be resized, + // then just set the display width according to the column's desired width + if (!this.OwningGrid.UsesStarSizing || (!column.ActualCanUserResize && !column.Width.IsStar)) + { + // In the edge-case where we're given infinite width and we have star columns, the + // star columns grow to their predefined limit of 10,000 (or their MaxWidth) + double newDisplayWidth = column.Width.IsStar ? + Math.Min(column.ActualMaxWidth, DataGrid.DATAGRID_maximumStarColumnWidth) : + Math.Max(column.ActualMinWidth, Math.Min(column.ActualMaxWidth, column.Width.DesiredValue)); + column.SetWidthDisplayValue(newDisplayWidth); + } + + // If we're auto-growing the column based on the header content, we want to measure it at its maximum value + if (autoGrowWidth) + { + columnHeader.Measure(new Size(column.ActualMaxWidth, double.PositiveInfinity)); + this.OwningGrid.AutoSizeColumn(column, columnHeader.DesiredSize.Width); + column.ComputeLayoutRoundedWidth(totalDisplayWidth); + } + else if (!this.OwningGrid.UsesStarSizing) + { + column.ComputeLayoutRoundedWidth(totalDisplayWidth); + columnHeader.Measure(new Size(column.LayoutRoundedWidth, double.PositiveInfinity)); + } + + // We need to track the largest height in order to auto-size + if (autoSizeHeight) + { + height = Math.Max(height, columnHeader.DesiredSize.Height); + } + + totalDisplayWidth += column.ActualWidth; + } + + // If we're using star sizing (and we're not waiting for an auto-column to finish growing) + // then we will resize all the columns to fit the available space. + if (this.OwningGrid.UsesStarSizing && !this.OwningGrid.AutoSizingColumns) + { + double adjustment = double.IsPositiveInfinity(availableSize.Width) ? this.OwningGrid.CellsWidth : availableSize.Width - totalDisplayWidth; + this.OwningGrid.AdjustColumnWidths(0, adjustment, false); + + // Since we didn't know the final widths of the columns until we resized, + // we waited until now to measure each header + double leftEdge = 0; + foreach (var column in this.OwningGrid.ColumnsInternal.GetVisibleColumns()) + { + column.ComputeLayoutRoundedWidth(leftEdge); + column.HeaderCell.Measure(new Size(column.LayoutRoundedWidth, double.PositiveInfinity)); + if (autoSizeHeight) + { + height = Math.Max(height, column.HeaderCell.DesiredSize.Height); + } + + leftEdge += column.ActualWidth; + } + } + + // Add the filler column if it's not represented. We won't know whether we need it or not until Arrange + DataGridFillerColumn fillerColumn = this.OwningGrid.ColumnsInternal.FillerColumn; + if (!fillerColumn.IsRepresented) + { + Debug.Assert(!this.Children.Contains(fillerColumn.HeaderCell), "Unexpected parent for filler column header cell."); + fillerColumn.HeaderCell.SeparatorVisibility = Visibility.Collapsed; + this.Children.Insert(this.OwningGrid.ColumnsInternal.Count, fillerColumn.HeaderCell); + fillerColumn.IsRepresented = true; + + // Optimize for the case where we don't need the filler cell + fillerColumn.HeaderCell.Visibility = Visibility.Collapsed; + } + + fillerColumn.HeaderCell.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + if (this.DragIndicator != null) + { + this.DragIndicator.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + if (this.DropLocationIndicator != null) + { + this.DropLocationIndicator.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + this.OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); + return new Size(this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, height); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridColumnHeadersPresenterAutomationPeer(this); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnReorderingEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnReorderingEventArgs.cs new file mode 100644 index 0000000..a97d548 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumnReorderingEventArgs.cs @@ -0,0 +1,51 @@ +// 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.ComponentModel; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for the event. + /// + public class DataGridColumnReorderingEventArgs : CancelEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The column that the event occurs for. + public DataGridColumnReorderingEventArgs(DataGridColumn dataGridColumn) + { + this.Column = dataGridColumn; + } + + /// + /// Gets the column being moved. + /// + public DataGridColumn Column + { + get; + private set; + } + + /// + /// Gets or sets the popup indicator displayed while dragging. If null and Handled = true, then do not display a tooltip. + /// + public Control DragIndicator + { + get; + set; + } + + /// + /// Gets or sets the Control to display at the insertion position. If null and Handled = true, then do not display an insertion indicator. + /// + public Control DropLocationIndicator + { + get; + set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumns.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumns.cs new file mode 100644 index 0000000..bb43011 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridColumns.cs @@ -0,0 +1,1914 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Control to represent data in columns and rows. + /// + public partial class DataGrid + { + /// + /// OnColumnDisplayIndexChanged + /// + /// Event arguments. + protected virtual void OnColumnDisplayIndexChanged(DataGridColumnEventArgs e) + { + this.ColumnDisplayIndexChanged?.Invoke(this, e); + } + + /// + /// OnColumnReordered + /// + /// Event arguments. + protected internal virtual void OnColumnReordered(DataGridColumnEventArgs e) + { + this.EnsureVerticalGridLines(); + + this.ColumnReordered?.Invoke(this, e); + } + + /// + /// OnColumnReordering + /// + /// Event arguments. + protected internal virtual void OnColumnReordering(DataGridColumnReorderingEventArgs e) + { + this.ColumnReordering?.Invoke(this, e); + } + + /// + /// OnColumnSorting + /// + /// Event arguments. + protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e) + { + this.Sorting?.Invoke(this, e); + } + + // Returns the column's width + internal static double GetEdgedColumnWidth(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + return dataGridColumn.ActualWidth; + } + + /// + /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) + { + if (!DoubleUtil.IsZero(amount)) + { + if (amount < 0) + { + amount = DecreaseColumnWidths(displayIndex, amount, userInitiated); + } + else + { + amount = IncreaseColumnWidths(displayIndex, amount, userInitiated); + } + } + + return amount; + } + + /// + /// Grows an auto-column's width to the desired width. + /// + /// Auto-column to adjust. + /// The new desired width of the column. + internal void AutoSizeColumn(DataGridColumn column, double desiredWidth) + { + Debug.Assert( + column.Width.IsAuto || column.Width.IsSizeToCells || column.Width.IsSizeToHeader || (!this.UsesStarSizing && column.Width.IsStar), + "Expected column.Width.IsAuto or column.Width.IsSizeToCells or column.Width.IsSizeToHeader or (!UsesStarSizing && column.Width.IsStar)."); + + // If we're using star sizing and this is the first time we've measured this particular auto-column, + // we want to allow all rows to get measured before we setup the star widths. We won't know the final + // desired value of the column until all rows have been measured. Because of this, we wait until + // an Arrange occurs before we adjust star widths. + if (this.UsesStarSizing && !column.IsInitialDesiredWidthDetermined) + { + this.AutoSizingColumns = true; + } + + // Update the column's DesiredValue if it needs to grow to fit the new desired value + if (desiredWidth > column.Width.DesiredValue || double.IsNaN(column.Width.DesiredValue)) + { + // If this auto-growth occurs after the column's initial desired width has been determined, + // then the growth should act like a resize (squish columns to the right). Otherwise, if + // this column is newly added, we'll just set its display value directly. + if (this.UsesStarSizing && column.IsInitialDesiredWidthDetermined) + { + column.Resize(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth, false); + } + else + { + column.SetWidthInternalNoCallback(new DataGridLength(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth)); + this.OnColumnWidthChanged(column); + } + } + } + + internal bool ColumnRequiresRightGridLine(DataGridColumn dataGridColumn, bool includeLastRightGridLineWhenPresent) + { + return (this.GridLinesVisibility == DataGridGridLinesVisibility.Vertical || this.GridLinesVisibility == DataGridGridLinesVisibility.All) && this.VerticalGridLinesBrush != null && + (dataGridColumn != this.ColumnsInternal.LastVisibleColumn || (includeLastRightGridLineWhenPresent && this.ColumnsInternal.FillerColumn.IsActive)); + } + + internal DataGridColumnCollection CreateColumnsInstance() + { + return new DataGridColumnCollection(this); + } + + /// + /// Decreases the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is decreased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Amount to decrease (in pixels). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double DecreaseColumnWidths(int displayIndex, double amount, bool userInitiated) + { + // 1. Take space from non-star columns with widths larger than desired widths (left to right). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false); + + // 2. Take space from star columns until they reach their min. + amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated); + + // 3. Take space from non-star columns that have already been initialized, until they reach their min (right to left). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, false); + + // 4. Take space from all non-star columns until they reach their min, even if they are new (right to left). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, true); + + return amount; + } + + internal bool GetColumnReadOnlyState(DataGridColumn dataGridColumn, bool isReadOnly) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + + DataGridBoundColumn dataGridBoundColumn = dataGridColumn as DataGridBoundColumn; + if (dataGridBoundColumn != null && dataGridBoundColumn.Binding != null) + { + string path = null; + if (dataGridBoundColumn.Binding.Path != null) + { + path = dataGridBoundColumn.Binding.Path.Path; + } + + if (!string.IsNullOrEmpty(path)) + { + return this.DataConnection.GetPropertyIsReadOnly(path) || isReadOnly; + } + } + + return isReadOnly; + } + + /// + /// Increases the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is increased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Amount of increase (in pixels). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double IncreaseColumnWidths(int displayIndex, double amount, bool userInitiated) + { + // 1. Give space to non-star columns that are smaller than their desired widths (left to right). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false); + + // 2. Give space to star columns until they reach their max. + amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated); + + // 3. Give space to non-star columns that have already been initialized, until they reach their max (right to left). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false); + + // 4. Give space to all non-star columns until they reach their max, even if they are new (right to left). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false); + + return amount; + } + + internal void OnClearingColumns() + { + // Rows need to be cleared first. There cannot be rows without also having columns. + ClearRows(false); + + // Removing all the column header cells + RemoveDisplayedColumnHeaders(); + + _horizontalOffset = _negHorizontalOffset = 0; + + if (_hScrollBar != null && _hScrollBar.Visibility == Visibility.Visible) + { + _hScrollBar.Value = 0; + } + } + + /// + /// Invalidates the widths of all columns because the resizing behavior of an individual column has changed. + /// + /// Column with CanUserResize property that has changed. + internal void OnColumnCanUserResizeChanged(DataGridColumn column) + { + if (column.IsVisible) + { + EnsureHorizontalLayout(); + } + } + + internal void OnColumnCellStyleChanged(DataGridColumn column, Style previousStyle) + { + // Set HeaderCell.Style for displayed rows if HeaderCell.Style is not already set + foreach (DataGridRow row in GetAllRows()) + { + row.Cells[column.Index].EnsureStyle(previousStyle); + } + + InvalidateRowHeightEstimate(); + } + + internal void OnColumnCollectionChanged_PostNotification(bool columnsGrew) + { + if (columnsGrew && + this.CurrentColumnIndex == -1) + { + MakeFirstDisplayedCellCurrentCell(); + } + + if (_autoGeneratingColumnOperationCount == 0) + { + EnsureRowsPresenterVisibility(); + InvalidateRowHeightEstimate(); + } + } + + internal void OnColumnCollectionChanged_PreNotification(bool columnsGrew) + { + // dataGridColumn==null means the collection was refreshed. + if (columnsGrew && _autoGeneratingColumnOperationCount == 0 && this.ColumnsItemsInternal.Count == 1) + { + RefreshRows(false /*recycleRows*/, true /*clearRows*/); + } + else + { + InvalidateMeasure(); + } + } + + internal void OnColumnDisplayIndexChanged(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + DataGridColumnEventArgs e = new DataGridColumnEventArgs(dataGridColumn); + + // Call protected method to raise event + if (dataGridColumn != this.ColumnsInternal.RowGroupSpacerColumn) + { + OnColumnDisplayIndexChanged(e); + } + } + + internal void OnColumnDisplayIndexChanged_PostNotification() + { + // Notifications for adjusted display indexes. + FlushDisplayIndexChanged(true /*raiseEvent*/); + + // Our displayed columns may have changed so recompute them + UpdateDisplayedColumns(); + + // Invalidate layout + CorrectColumnFrozenStates(); + EnsureHorizontalLayout(); + } + + internal void OnColumnDisplayIndexChanging(DataGridColumn targetColumn, int newDisplayIndex) + { + Debug.Assert(targetColumn != null, "Expected non-null targetColumn."); + Debug.Assert(newDisplayIndex != targetColumn.DisplayIndexWithFiller, "Expected newDisplayIndex other than targetColumn.DisplayIndexWithFiller."); + + if (InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing display indexes while adjusting them. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + try + { + InDisplayIndexAdjustments = true; + + bool trackChange = targetColumn != this.ColumnsInternal.RowGroupSpacerColumn; + DataGridColumn column; + + // Move is legal - let's adjust the affected display indexes. + if (newDisplayIndex < targetColumn.DisplayIndexWithFiller) + { + // DisplayIndex decreases. All columns with newDisplayIndex <= DisplayIndex < targetColumn.DisplayIndex + // get their DisplayIndex incremented. + for (int i = newDisplayIndex; i < targetColumn.DisplayIndexWithFiller; i++) + { + column = this.ColumnsInternal.GetColumnAtDisplayIndex(i); + column.DisplayIndexWithFiller = column.DisplayIndexWithFiller + 1; + if (trackChange) + { + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + } + else + { + // DisplayIndex increases. All columns with targetColumn.DisplayIndex < DisplayIndex <= newDisplayIndex + // get their DisplayIndex decremented. + for (int i = newDisplayIndex; i > targetColumn.DisplayIndexWithFiller; i--) + { + column = this.ColumnsInternal.GetColumnAtDisplayIndex(i); + column.DisplayIndexWithFiller = column.DisplayIndexWithFiller - 1; + if (trackChange) + { + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + } + + // Now let's actually change the order of the DisplayIndexMap + if (targetColumn.DisplayIndexWithFiller != -1) + { + this.ColumnsInternal.DisplayIndexMap.Remove(targetColumn.Index); + } + + this.ColumnsInternal.DisplayIndexMap.Insert(newDisplayIndex, targetColumn.Index); + } + finally + { + InDisplayIndexAdjustments = false; + } + + // Note that displayIndex of moved column is updated by caller. + } + + internal void OnColumnBindingChanged(DataGridBoundColumn column) + { + // Update Binding in Displayed rows by regenerating the affected elements + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + PopulateCellContent(false /*isCellEdited*/, column, row, row.Cells[column.Index]); + } + } + } + + internal void OnColumnElementStyleChanged(DataGridBoundColumn column) + { + // Update Element Style in Displayed rows + foreach (DataGridRow row in GetAllRows()) + { + FrameworkElement element = column.GetCellContent(row); + if (element != null) + { + element.SetStyleWithType(column.ElementStyle); + } + } + + InvalidateRowHeightEstimate(); + } + + internal void OnColumnHeaderDragStarted(DragStartedEventArgs e) + { + if (this.ColumnHeaderDragStarted != null) + { + this.ColumnHeaderDragStarted(this, e); + } + } + + internal void OnColumnHeaderDragDelta(DragDeltaEventArgs e) + { + if (this.ColumnHeaderDragDelta != null) + { + this.ColumnHeaderDragDelta(this, e); + } + } + + internal void OnColumnHeaderDragCompleted(DragCompletedEventArgs e) + { + if (this.ColumnHeaderDragCompleted != null) + { + this.ColumnHeaderDragCompleted(this, e); + } + } + + /// + /// Adjusts the specified column's width according to its new maximum value. + /// + /// The column to adjust. + /// The old ActualMaxWidth of the column. + internal void OnColumnMaxWidthChanged(DataGridColumn column, double oldValue) + { + Debug.Assert(column != null, "Expected non-null column."); + + if (column.Visibility == Visibility.Visible && oldValue != column.ActualMaxWidth) + { + if (column.ActualMaxWidth < column.Width.DisplayValue) + { + // If the maximum width has caused the column to decrease in size, try first to resize + // the columns to the right to make up for the difference in width, but don't limit the column's + // final display value to how much they could be resized. + AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMaxWidth, false); + column.SetWidthDisplayValue(column.ActualMaxWidth); + } + else if (column.Width.DisplayValue == oldValue && column.Width.DesiredValue > column.Width.DisplayValue) + { + // If the column was previously limited by its maximum value but has more room now, + // attempt to resize the column to its desired width. + column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false); + } + + OnColumnWidthChanged(column); + } + } + + /// + /// Adjusts the specified column's width according to its new minimum value. + /// + /// The column to adjust. + /// The old ActualMinWidth of the column. + internal void OnColumnMinWidthChanged(DataGridColumn column, double oldValue) + { + Debug.Assert(column != null, "Expected non-null column."); + + if (column.Visibility == Visibility.Visible && oldValue != column.ActualMinWidth) + { + if (column.ActualMinWidth > column.Width.DisplayValue) + { + // If the minimum width has caused the column to increase in size, try first to resize + // the columns to the right to make up for the difference in width, but don't limit the column's + // final display value to how much they could be resized. + AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMinWidth, false); + column.SetWidthDisplayValue(column.ActualMinWidth); + } + else if (column.Width.DisplayValue == oldValue && column.Width.DesiredValue < column.Width.DisplayValue) + { + // If the column was previously limited by its minimum value but can be smaller now, + // attempt to resize the column to its desired width. + column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false); + } + + OnColumnWidthChanged(column); + } + } + + internal void OnColumnReadOnlyStateChanging(DataGridColumn dataGridColumn, bool isReadOnly) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + if (isReadOnly && this.CurrentColumnIndex == dataGridColumn.Index) + { + // Edited column becomes read-only. Exit editing mode. + if (!EndCellEdit(DataGridEditAction.Commit, true /*exitEditingMode*/, this.ContainsFocus /*keepFocus*/, true /*raiseEvents*/)) + { + EndCellEdit(DataGridEditAction.Cancel, true /*exitEditingMode*/, this.ContainsFocus /*keepFocus*/, false /*raiseEvents*/); + } + } + } + + internal void OnColumnVisibleStateChanged(DataGridColumn updatedColumn) + { + Debug.Assert(updatedColumn != null, "Expected non-null updatedColumn."); + + CorrectColumnFrozenStates(); + UpdateDisplayedColumns(); + EnsureRowsPresenterVisibility(); + EnsureHorizontalLayout(); + InvalidateColumnHeadersMeasure(); + + if (updatedColumn.IsVisible && + this.ColumnsInternal.VisibleColumnCount == 1 && this.CurrentColumnIndex == -1) + { + Debug.Assert(this.SelectedIndex == this.DataConnection.IndexOf(this.SelectedItem), "Expected SelectedIndex equals DataConnection.IndexOf(this.SelectedItem)."); + if (this.SelectedIndex != -1) + { + SetAndSelectCurrentCell(updatedColumn.Index, this.SelectedIndex, true /*forceCurrentCellSelection*/); + } + else + { + MakeFirstDisplayedCellCurrentCell(); + } + } + + // We need to explicitly collapse the cells of the invisible column because layout only goes through + // visible ones + if (updatedColumn.Visibility == Visibility.Collapsed) + { + foreach (DataGridRow row in GetAllRows()) + { + row.Cells[updatedColumn.Index].Visibility = Visibility.Collapsed; + } + } + } + + internal void OnColumnVisibleStateChanging(DataGridColumn targetColumn) + { + Debug.Assert(targetColumn != null, "Expected non-null targetColumn."); + + if (targetColumn.IsVisible && this.CurrentColumn == targetColumn) + { + // Column of the current cell is made invisible. Trying to move the current cell to a neighbor column. May throw an exception. + DataGridColumn dataGridColumn = this.ColumnsInternal.GetNextVisibleColumn(targetColumn); + if (dataGridColumn == null) + { + dataGridColumn = this.ColumnsInternal.GetPreviousVisibleNonFillerColumn(targetColumn); + } + + if (dataGridColumn == null) + { + SetCurrentCellCore(-1, -1); + } + else + { + SetCurrentCellCore(dataGridColumn.Index, this.CurrentSlot); + } + } + } + + internal void OnColumnWidthChanged(DataGridColumn updatedColumn) + { + Debug.Assert(updatedColumn != null, "Expected non-null updatedColumn."); + if (updatedColumn.IsVisible) + { + EnsureHorizontalLayout(); + } + } + + internal void OnFillerColumnWidthNeeded(double finalWidth) + { + DataGridFillerColumn fillerColumn = this.ColumnsInternal.FillerColumn; + double totalColumnsWidth = this.ColumnsInternal.VisibleEdgedColumnsWidth; + if (finalWidth - totalColumnsWidth > DATAGRID_roundingDelta) + { + fillerColumn.FillerWidth = finalWidth - totalColumnsWidth; + } + else + { + fillerColumn.FillerWidth = 0; + } + } + + internal void OnInsertedColumn_PostNotification(DataGridCellCoordinates newCurrentCellCoordinates, int newDisplayIndex) + { + // Update current cell if needed + if (newCurrentCellCoordinates.ColumnIndex != -1) + { + Debug.Assert(this.CurrentColumnIndex == -1, "Expected CurrentColumnIndex equals -1."); + SetAndSelectCurrentCell( + newCurrentCellCoordinates.ColumnIndex, + newCurrentCellCoordinates.Slot, + this.ColumnsInternal.VisibleColumnCount == 1 /*forceCurrentCellSelection*/); + + if (newDisplayIndex < this.FrozenColumnCountWithFiller) + { + CorrectColumnFrozenStates(); + } + } + } + + internal void OnInsertedColumn_PreNotification(DataGridColumn insertedColumn) + { + // Fix the Index of all following columns + CorrectColumnIndexesAfterInsertion(insertedColumn, 1); + + Debug.Assert(insertedColumn.Index >= 0, "Expected positive insertedColumn.Index."); + Debug.Assert(insertedColumn.Index < this.ColumnsItemsInternal.Count, "insertedColumn.Index smaller than ColumnsItemsInternal.Count."); + Debug.Assert(insertedColumn.OwningGrid == this, "Expected insertedColumn.OwningGrid equals this DataGrid."); + + CorrectColumnDisplayIndexesAfterInsertion(insertedColumn); + + InsertDisplayedColumnHeader(insertedColumn); + + // Insert the missing data cells + if (this.SlotCount > 0) + { + int newColumnCount = this.ColumnsItemsInternal.Count; + + foreach (DataGridRow row in GetAllRows()) + { + if (row.Cells.Count < newColumnCount) + { + AddNewCellPrivate(row, insertedColumn); + } + } + } + + if (insertedColumn.IsVisible) + { + EnsureHorizontalLayout(); + } + + DataGridBoundColumn boundColumn = insertedColumn as DataGridBoundColumn; + if (boundColumn != null && !boundColumn.IsAutoGenerated) + { + boundColumn.SetHeaderFromBinding(); + } + } + + internal DataGridCellCoordinates OnInsertingColumn(int columnIndexInserted, DataGridColumn insertColumn) + { + DataGridCellCoordinates newCurrentCellCoordinates; + Debug.Assert(insertColumn != null, "Expected non-null insertColumn."); + + if (insertColumn.OwningGrid != null && insertColumn != this.ColumnsInternal.RowGroupSpacerColumn) + { + throw DataGridError.DataGrid.ColumnCannotBeReassignedToDifferentDataGrid(); + } + + // Reset current cell if there is one, no matter the relative position of the columns involved + if (this.CurrentColumnIndex != -1) + { + _temporarilyResetCurrentCell = true; + newCurrentCellCoordinates = new DataGridCellCoordinates( + columnIndexInserted <= this.CurrentColumnIndex ? this.CurrentColumnIndex + 1 : this.CurrentColumnIndex, + this.CurrentSlot); + ResetCurrentCellCore(); + } + else + { + newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); + } + + return newCurrentCellCoordinates; + } + + internal void OnRemovedColumn_PostNotification(DataGridCellCoordinates newCurrentCellCoordinates) + { + // Update current cell if needed + if (newCurrentCellCoordinates.ColumnIndex != -1) + { + Debug.Assert(this.CurrentColumnIndex == -1, "Expected CurrentColumnIndex equals -1."); + SetAndSelectCurrentCell(newCurrentCellCoordinates.ColumnIndex, newCurrentCellCoordinates.Slot, false /*forceCurrentCellSelection*/); + } + } + + internal void OnRemovedColumn_PreNotification(DataGridColumn removedColumn) + { + Debug.Assert(removedColumn.Index >= 0, "Expected positive removedColumn.Index."); + Debug.Assert(removedColumn.OwningGrid == null, "Expected null removedColumn.OwningGrid."); + + // Intentionally keep the DisplayIndex intact after detaching the column. + CorrectColumnIndexesAfterDeletion(removedColumn); + + CorrectColumnDisplayIndexesAfterDeletion(removedColumn); + + // If the detached column was frozen, a new column needs to take its place + if (removedColumn.IsFrozen) + { + removedColumn.IsFrozen = false; + CorrectColumnFrozenStates(); + } + + UpdateDisplayedColumns(); + + // Fix the existing rows by removing cells at correct index + int newColumnCount = this.ColumnsItemsInternal.Count; + + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + if (row.Cells.Count > newColumnCount) + { + row.Cells.RemoveAt(removedColumn.Index); + } + } + + _rowsPresenter.InvalidateArrange(); + } + + RemoveDisplayedColumnHeader(removedColumn); + } + + internal DataGridCellCoordinates OnRemovingColumn(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + Debug.Assert(dataGridColumn.Index >= 0, "Expected positive dataGridColumn.Index."); + Debug.Assert(dataGridColumn.Index < this.ColumnsItemsInternal.Count, "Expected dataGridColumn.Index smaller than ColumnsItemsInternal.Count."); + + DataGridCellCoordinates newCurrentCellCoordinates; + + _temporarilyResetCurrentCell = false; + int columnIndex = dataGridColumn.Index; + + // Reset the current cell's address if there is one. + if (this.CurrentColumnIndex != -1) + { + int newCurrentColumnIndex = this.CurrentColumnIndex; + if (columnIndex == newCurrentColumnIndex) + { + DataGridColumn dataGridColumnNext = this.ColumnsInternal.GetNextVisibleColumn(this.ColumnsItemsInternal[columnIndex]); + if (dataGridColumnNext != null) + { + if (dataGridColumnNext.Index > columnIndex) + { + newCurrentColumnIndex = dataGridColumnNext.Index - 1; + } + else + { + newCurrentColumnIndex = dataGridColumnNext.Index; + } + } + else + { + DataGridColumn dataGridColumnPrevious = this.ColumnsInternal.GetPreviousVisibleNonFillerColumn(this.ColumnsItemsInternal[columnIndex]); + if (dataGridColumnPrevious != null) + { + if (dataGridColumnPrevious.Index > columnIndex) + { + newCurrentColumnIndex = dataGridColumnPrevious.Index - 1; + } + else + { + newCurrentColumnIndex = dataGridColumnPrevious.Index; + } + } + else + { + newCurrentColumnIndex = -1; + } + } + } + else if (columnIndex < newCurrentColumnIndex) + { + newCurrentColumnIndex--; + } + + newCurrentCellCoordinates = new DataGridCellCoordinates(newCurrentColumnIndex, (newCurrentColumnIndex == -1) ? -1 : this.CurrentSlot); + if (columnIndex == this.CurrentColumnIndex) + { + // If the commit fails, force a cancel edit + if (!this.CommitEdit(DataGridEditingUnit.Row, false /*exitEditingMode*/)) + { + this.CancelEdit(DataGridEditingUnit.Row, false /*raiseEvents*/); + } + } + else + { + // Underlying data of deleted column is gone. It cannot be accessed anymore. + // Do not end editing mode so that CellValidation doesn't get raised, since that event needs the current formatted value. + _temporarilyResetCurrentCell = true; + } + + bool success = this.SetCurrentCellCore(-1, -1); + Debug.Assert(success, "Expected successful call to SetCurrentCellCore."); + } + else + { + newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); + } + + // If the last column is removed, delete all the rows first. + if (this.ColumnsItemsInternal.Count == 1) + { + ClearRows(false); + } + + // Is deleted column scrolled off screen? + if (dataGridColumn.IsVisible && + !dataGridColumn.IsFrozen && + this.DisplayData.FirstDisplayedScrollingCol >= 0) + { + // Deleted column is part of scrolling columns. + if (this.DisplayData.FirstDisplayedScrollingCol == dataGridColumn.Index) + { + // Deleted column is first scrolling column + _horizontalOffset -= _negHorizontalOffset; + _negHorizontalOffset = 0; + } + else if (!this.ColumnsInternal.DisplayInOrder(this.DisplayData.FirstDisplayedScrollingCol, dataGridColumn.Index)) + { + // Deleted column is displayed before first scrolling column + Debug.Assert(_horizontalOffset >= GetEdgedColumnWidth(dataGridColumn), "Expected _horizontalOffset greater than or equal to GetEdgedColumnWidth(dataGridColumn)."); + _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn); + } + + if (_hScrollBar != null && _hScrollBar.Visibility == Visibility.Visible) + { + _hScrollBar.Value = _horizontalOffset; + } + } + + return newCurrentCellCoordinates; + } + + /// + /// Called when a column property changes, and its cells need to adjust that column change. + /// + internal void RefreshColumnElements(DataGridColumn dataGridColumn, string propertyName) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + + // Take care of the non-displayed loaded rows + for (int index = 0; index < _loadedRows.Count;) + { + DataGridRow dataGridRow = _loadedRows[index]; + Debug.Assert(dataGridRow != null, "Expected non-null dataGridRow."); + if (!this.IsSlotVisible(dataGridRow.Slot)) + { + RefreshCellElement(dataGridColumn, dataGridRow, propertyName); + } + + index++; + } + + // Take care of the displayed rows + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + RefreshCellElement(dataGridColumn, row, propertyName); + } + + // This update could change layout so we need to update our estimate and invalidate + InvalidateRowHeightEstimate(); + InvalidateMeasure(); + } + } + + /// + /// Decreases the width of a non-star column by the given amount, if possible. If the total desired + /// adjustment amount could not be met, the remaining amount of adjustment is returned. The adjustment + /// stops when the column's target width has been met. + /// + /// Column to adjust. + /// The target width of the column (in pixels). + /// Amount to decrease (in pixels). + /// The remaining amount of adjustment. + private static double DecreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount) + { + Debug.Assert(amount < 0, "Expected negative amount."); + Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star, "column.Width.UnitType other than DataGridLengthUnitType.Star."); + + if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) + { + return amount; + } + + double adjustment = Math.Max( + column.ActualMinWidth - column.Width.DisplayValue, + Math.Max(targetWidth - column.Width.DisplayValue, amount)); + + column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment); + return amount - adjustment; + } + + private static DataGridAutoGeneratingColumnEventArgs GenerateColumn(Type propertyType, string propertyName, string header) + { + // Create a new DataBoundColumn for the Property + DataGridBoundColumn newColumn = GetDataGridColumnFromType(propertyType); + Binding binding = new Binding(); + binding.Path = new PropertyPath(propertyName); + newColumn.Binding = binding; + newColumn.Header = header; + newColumn.IsAutoGenerated = true; + return new DataGridAutoGeneratingColumnEventArgs(propertyName, propertyType, newColumn); + } + + private static DataGridBoundColumn GetDataGridColumnFromType(Type type) + { + Debug.Assert(type != null, "Expected non-null type."); + if (type == typeof(bool)) + { + return new DataGridCheckBoxColumn(); + } + else if (type == typeof(bool?)) + { + DataGridCheckBoxColumn column = new DataGridCheckBoxColumn(); + column.IsThreeState = true; + return column; + } + + return new DataGridTextColumn(); + } + + /// + /// Increases the width of a non-star column by the given amount, if possible. If the total desired + /// adjustment amount could not be met, the remaining amount of adjustment is returned. The adjustment + /// stops when the column's target width has been met. + /// + /// Column to adjust. + /// The target width of the column (in pixels). + /// Amount to increase (in pixels). + /// The remaining amount of adjustment. + private static double IncreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount) + { + Debug.Assert(amount > 0, "Expected strictly positive amount."); + Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star, "Expected column.Width.UnitType other than DataGridLengthUnitType.Star."); + + if (targetWidth <= column.Width.DisplayValue) + { + return amount; + } + + double adjustment = Math.Min( + column.ActualMaxWidth - column.Width.DisplayValue, + Math.Min(targetWidth - column.Width.DisplayValue, amount)); + + column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment); + return amount - adjustment; + } + + private static void RefreshCellElement(DataGridColumn dataGridColumn, DataGridRow dataGridRow, string propertyName) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + Debug.Assert(dataGridRow != null, "Expected non-null dataGridRow."); + + DataGridCell dataGridCell = dataGridRow.Cells[dataGridColumn.Index]; + Debug.Assert(dataGridCell != null, "Expected non-null dataGridCell."); + FrameworkElement element = dataGridCell.Content as FrameworkElement; + if (element != null) + { + dataGridColumn.RefreshCellContent(element, dataGridRow.ComputedForeground, propertyName); + } + } + + private bool AddGeneratedColumn(DataGridAutoGeneratingColumnEventArgs e) + { + // Raise the AutoGeneratingColumn event in case the user wants to Cancel or Replace the + // column being generated + OnAutoGeneratingColumn(e); + if (e.Cancel) + { + return false; + } + else + { + if (e.Column != null) + { + // Set the IsAutoGenerated flag here in case the user provides a custom auto-generated column + e.Column.IsAutoGenerated = true; + } + + this.ColumnsInternal.Add(e.Column); + this.ColumnsInternal.AutogeneratedColumnCount++; + return true; + } + } + + /// + /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) + { + double remainingAdjustment = adjustment; + if (DoubleUtil.IsZero(remainingAdjustment)) + { + return remainingAdjustment; + } + + bool increase = remainingAdjustment > 0; + + // Make an initial pass through the star columns to total up some values. + bool scaleStarWeights = false; + double totalStarColumnsWidth = 0; + double totalStarColumnsWidthLimit = 0; + double totalStarWeights = 0; + List starColumns = new List(); + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns(c => c.Width.IsStar && c.IsVisible && (c.ActualCanUserResize || !userInitiated))) + { + if (column.DisplayIndex < displayIndex) + { + scaleStarWeights = true; + continue; + } + + starColumns.Add(column); + totalStarWeights += column.Width.Value; + totalStarColumnsWidth += column.Width.DisplayValue; + totalStarColumnsWidthLimit += increase ? column.ActualMaxWidth : column.ActualMinWidth; + } + + // Set the new desired widths according to how much all the star columns can be adjusted without any + // of them being limited by their minimum or maximum widths (as that would distort their ratios). + double adjustmentLimit = totalStarColumnsWidthLimit - totalStarColumnsWidth; + adjustmentLimit = increase ? Math.Min(adjustmentLimit, adjustment) : Math.Max(adjustmentLimit, adjustment); + foreach (DataGridColumn starColumn in starColumns) + { + starColumn.SetWidthDesiredValue((totalStarColumnsWidth + adjustmentLimit) * starColumn.Width.Value / totalStarWeights); + } + + // Adjust the star column widths first towards their desired values, and then towards their limits. + remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => c.Width.DesiredValue); + remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => increase ? c.ActualMaxWidth : c.ActualMinWidth); + + // Set the new star value weights according to how much the total column widths have changed. + // Only do this if there were other star columns to the left, though. If there weren't any then that means + // all the star columns were adjusted at the same time, and therefore, their ratios have not changed. + if (scaleStarWeights) + { + double starRatio = (totalStarColumnsWidth + adjustment - remainingAdjustment) / totalStarColumnsWidth; + foreach (DataGridColumn starColumn in starColumns) + { + starColumn.SetWidthStarValue(Math.Min(double.MaxValue, starRatio * starColumn.Width.Value)); + } + } + + return remainingAdjustment; + } + + /// + /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The columns will stop adjusting + /// once they hit their target widths. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The target width of the column. + /// The remaining amount of adjustment. + private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth) + { + if (DoubleUtil.IsZero(remainingAdjustment)) + { + return remainingAdjustment; + } + + bool increase = remainingAdjustment > 0; + + double totalStarWeights = 0; + double totalStarColumnsWidth = 0; + + // Order the star columns according to which one will hit their target width (or min/max limit) first. + // Each KeyValuePair represents a column (as the key) and an ordering factor (as the value). The ordering factor + // is computed based on the distance from each column's current display width to its target width. Because each column + // could have different star ratios, though, this distance is then adjusted according to its star value. A column with + // a larger star value, for example, will change size more rapidly than a column with a lower star value. + List> starColumnPairs = new List>(); + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns( + c => c.Width.IsStar && c.DisplayIndex >= displayIndex && c.IsVisible && c.Width.Value > 0 && (c.ActualCanUserResize || !userInitiated))) + { + int insertIndex = 0; + double distanceToTarget = Math.Min(column.ActualMaxWidth, Math.Max(targetWidth(column), column.ActualMinWidth)) - column.Width.DisplayValue; + double factor = (increase ? Math.Max(0, distanceToTarget) : Math.Min(0, distanceToTarget)) / column.Width.Value; + foreach (KeyValuePair starColumnPair in starColumnPairs) + { + if (increase ? factor <= starColumnPair.Value : factor >= starColumnPair.Value) + { + break; + } + + insertIndex++; + } + + starColumnPairs.Insert(insertIndex, new KeyValuePair(column, factor)); + totalStarWeights += column.Width.Value; + totalStarColumnsWidth += column.Width.DisplayValue; + } + + // Adjust the column widths one at a time until they either hit their individual target width + // or the total remaining amount to adjust has been depleted. + foreach (KeyValuePair starColumnPair in starColumnPairs) + { + double distanceToTarget = starColumnPair.Value * starColumnPair.Key.Width.Value; + double distanceAvailable = (starColumnPair.Key.Width.Value * remainingAdjustment) / totalStarWeights; + double adjustment = increase ? Math.Min(distanceToTarget, distanceAvailable) : Math.Max(distanceToTarget, distanceAvailable); + + remainingAdjustment -= adjustment; + totalStarWeights -= starColumnPair.Key.Width.Value; + starColumnPair.Key.SetWidthDisplayValue(Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, starColumnPair.Key.Width.DisplayValue + adjustment)); + } + + return remainingAdjustment; + } + + private void AutoGenerateColumnsPrivate() + { + if (!_measured || (_autoGeneratingColumnOperationCount > 0)) + { + // Reading the DataType when we generate columns could cause the CollectionView to + // raise a Reset if its Enumeration changed. In that case, we don't want to generate again. + return; + } + + _autoGeneratingColumnOperationCount++; + try + { + // Always remove existing auto-generated columns before generating new ones + RemoveAutoGeneratedColumns(); + GenerateColumnsFromProperties(); + EnsureRowsPresenterVisibility(); + InvalidateRowHeightEstimate(); + } + finally + { + _autoGeneratingColumnOperationCount--; + } + } + + private bool ComputeDisplayedColumns() + { + bool invalidate = false; + int visibleScrollingColumnsTmp = 0; + double displayWidth = this.CellsWidth; + double cx = 0; + int firstDisplayedFrozenCol = -1; + int firstDisplayedScrollingCol = this.DisplayData.FirstDisplayedScrollingCol; + + // the same problem with negative numbers: + // if the width passed in is negative, then return 0 + if (displayWidth <= 0 || this.ColumnsInternal.VisibleColumnCount == 0) + { + this.DisplayData.FirstDisplayedScrollingCol = -1; + this.DisplayData.LastTotallyDisplayedScrollingCol = -1; + return invalidate; + } + + foreach (DataGridColumn dataGridColumn in this.ColumnsInternal.GetVisibleFrozenColumns()) + { + if (firstDisplayedFrozenCol == -1) + { + firstDisplayedFrozenCol = dataGridColumn.Index; + } + + cx += GetEdgedColumnWidth(dataGridColumn); + if (cx >= displayWidth) + { + break; + } + } + + Debug.Assert(cx <= this.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(), "cx smaller than or equal to ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth()."); + + if (cx < displayWidth && firstDisplayedScrollingCol >= 0) + { + DataGridColumn dataGridColumn = this.ColumnsItemsInternal[firstDisplayedScrollingCol]; + if (dataGridColumn.IsFrozen) + { + dataGridColumn = this.ColumnsInternal.FirstVisibleScrollingColumn; + _negHorizontalOffset = 0; + if (dataGridColumn == null) + { + this.DisplayData.FirstDisplayedScrollingCol = this.DisplayData.LastTotallyDisplayedScrollingCol = -1; + return invalidate; + } + else + { + firstDisplayedScrollingCol = dataGridColumn.Index; + } + } + + cx -= _negHorizontalOffset; + while (cx < displayWidth && dataGridColumn != null) + { + cx += GetEdgedColumnWidth(dataGridColumn); + visibleScrollingColumnsTmp++; + dataGridColumn = this.ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + } + + var numVisibleScrollingCols = visibleScrollingColumnsTmp; + + // if we inflate the data area then we paint columns to the left of firstDisplayedScrollingCol + if (cx < displayWidth) + { + Debug.Assert(firstDisplayedScrollingCol >= 0, "Expected positive firstDisplayedScrollingCol."); + + // first minimize value of _negHorizontalOffset + if (_negHorizontalOffset > 0) + { + invalidate = true; + if (displayWidth - cx > _negHorizontalOffset) + { + cx += _negHorizontalOffset; + _horizontalOffset -= _negHorizontalOffset; + if (_horizontalOffset < DATAGRID_roundingDelta) + { + // Snap to zero to avoid trying to partially scroll in first scrolled off column below + _horizontalOffset = 0; + } + + _negHorizontalOffset = 0; + } + else + { + _horizontalOffset -= displayWidth - cx; + _negHorizontalOffset -= displayWidth - cx; + cx = displayWidth; + } + + // Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset + // since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell + // called by DataGridCellsPresenter.MeasureOverride. + this.HorizontalAdjustment = Math.Min(this.HorizontalAdjustment, _horizontalOffset); + } + + // second try to scroll entire columns + if (cx < displayWidth && _horizontalOffset > 0) + { + Debug.Assert(_negHorizontalOffset == 0, "Expected _negHorizontalOffset equals 0."); + dataGridColumn = this.ColumnsInternal.GetPreviousVisibleScrollingColumn(this.ColumnsItemsInternal[firstDisplayedScrollingCol]); + while (dataGridColumn != null && cx + GetEdgedColumnWidth(dataGridColumn) <= displayWidth) + { + cx += GetEdgedColumnWidth(dataGridColumn); + visibleScrollingColumnsTmp++; + invalidate = true; + firstDisplayedScrollingCol = dataGridColumn.Index; + _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn); + dataGridColumn = this.ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumn); + } + } + + // third try to partially scroll in first scrolled off column + if (cx < displayWidth && _horizontalOffset > 0) + { + Debug.Assert(_negHorizontalOffset == 0, "Expected _negHorizontalOffset equals 0."); + dataGridColumn = this.ColumnsInternal.GetPreviousVisibleScrollingColumn(this.ColumnsItemsInternal[firstDisplayedScrollingCol]); + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + Debug.Assert(GetEdgedColumnWidth(dataGridColumn) > displayWidth - cx, "Expected GetEdgedColumnWidth(dataGridColumn) greater than displayWidth - cx."); + firstDisplayedScrollingCol = dataGridColumn.Index; + _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - displayWidth + cx; + _horizontalOffset -= displayWidth - cx; + visibleScrollingColumnsTmp++; + invalidate = true; + cx = displayWidth; + Debug.Assert(_negHorizontalOffset == GetNegHorizontalOffsetFromHorizontalOffset(_horizontalOffset), "Expected _negHorizontalOffset equals GetNegHorizontalOffsetFromHorizontalOffset(_horizontalOffset)."); + } + + // update the number of visible columns to the new reality + Debug.Assert(numVisibleScrollingCols <= visibleScrollingColumnsTmp, "Expected numVisibleScrollingCols less than or equal to visibleScrollingColumnsTmp."); + numVisibleScrollingCols = visibleScrollingColumnsTmp; + } + + int jumpFromFirstVisibleScrollingCol = numVisibleScrollingCols - 1; + if (cx > displayWidth) + { + jumpFromFirstVisibleScrollingCol--; + } + + Debug.Assert(jumpFromFirstVisibleScrollingCol >= -1, "Expected jumpFromFirstVisibleScrollingCol greater than or equal to -1."); + + if (jumpFromFirstVisibleScrollingCol < 0) + { + this.DisplayData.LastTotallyDisplayedScrollingCol = -1; // no totally visible scrolling column at all + } + else + { + Debug.Assert(firstDisplayedScrollingCol >= 0, "Expected positive firstDisplayedScrollingCol."); + dataGridColumn = this.ColumnsItemsInternal[firstDisplayedScrollingCol]; + for (int jump = 0; jump < jumpFromFirstVisibleScrollingCol; jump++) + { + dataGridColumn = this.ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + } + + this.DisplayData.LastTotallyDisplayedScrollingCol = dataGridColumn.Index; + } + } + else + { + this.DisplayData.LastTotallyDisplayedScrollingCol = -1; + } + + this.DisplayData.FirstDisplayedScrollingCol = firstDisplayedScrollingCol; + + return invalidate; + } + + private int ComputeFirstVisibleScrollingColumn() + { + if (this.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth() >= this.CellsWidth) + { + // Not enough room for scrolling columns. + _negHorizontalOffset = 0; + return -1; + } + + DataGridColumn dataGridColumn = this.ColumnsInternal.FirstVisibleScrollingColumn; + + if (_horizontalOffset == 0) + { + _negHorizontalOffset = 0; + return (dataGridColumn == null) ? -1 : dataGridColumn.Index; + } + + double cx = 0; + while (dataGridColumn != null) + { + cx += GetEdgedColumnWidth(dataGridColumn); + if (cx > _horizontalOffset) + { + break; + } + + dataGridColumn = this.ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + } + + if (dataGridColumn == null) + { + Debug.Assert(cx <= _horizontalOffset, "Expected cx less than or equal to _horizontalOffset."); + dataGridColumn = this.ColumnsInternal.FirstVisibleScrollingColumn; + if (dataGridColumn == null) + { + _negHorizontalOffset = 0; + return -1; + } + else + { + if (_negHorizontalOffset != _horizontalOffset) + { + _negHorizontalOffset = 0; + } + + return dataGridColumn.Index; + } + } + else + { + _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - (cx - _horizontalOffset); + return dataGridColumn.Index; + } + } + + private void CorrectColumnDisplayIndexesAfterDeletion(DataGridColumn deletedColumn) + { + // Column indexes have already been adjusted. + // This column has already been detached and has retained its old Index and DisplayIndex + Debug.Assert(deletedColumn != null, "Expected non-null deletedColumn."); + Debug.Assert(deletedColumn.OwningGrid == null, "Expected null deletedColumn.OwningGrid."); + Debug.Assert(deletedColumn.Index >= 0, "Expected positive deletedColumn.Index."); + Debug.Assert(deletedColumn.DisplayIndexWithFiller >= 0, "Expected positive deletedColumn.DisplayIndexWithFiller."); + + try + { + InDisplayIndexAdjustments = true; + + // The DisplayIndex of columns greater than the deleted column need to be decremented, + // as do the DisplayIndexMap values of modified column Indexes + DataGridColumn column; + this.ColumnsInternal.DisplayIndexMap.RemoveAt(deletedColumn.DisplayIndexWithFiller); + for (int displayIndex = 0; displayIndex < this.ColumnsInternal.DisplayIndexMap.Count; displayIndex++) + { + if (this.ColumnsInternal.DisplayIndexMap[displayIndex] > deletedColumn.Index) + { + this.ColumnsInternal.DisplayIndexMap[displayIndex]--; + } + + if (displayIndex >= deletedColumn.DisplayIndexWithFiller) + { + column = this.ColumnsInternal.GetColumnAtDisplayIndex(displayIndex); + column.DisplayIndexWithFiller = column.DisplayIndexWithFiller - 1; + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + +#if DEBUG + Debug.Assert(this.ColumnsInternal.Debug_VerifyColumnDisplayIndexes(), "Expected ColumnsInternal.Debug_VerifyColumnDisplayIndexes() is true."); +#endif + + // Now raise all the OnColumnDisplayIndexChanged events + FlushDisplayIndexChanged(true /*raiseEvent*/); + } + finally + { + InDisplayIndexAdjustments = false; + FlushDisplayIndexChanged(false /*raiseEvent*/); + } + } + + private void CorrectColumnDisplayIndexesAfterInsertion(DataGridColumn insertedColumn) + { + Debug.Assert(insertedColumn != null, "Expected non-null insertedColumn."); + Debug.Assert(insertedColumn.OwningGrid == this, "Expected insertedColumn.OwningGrid equals this DataGrid."); + if (insertedColumn.DisplayIndexWithFiller == -1 || insertedColumn.DisplayIndexWithFiller >= this.ColumnsItemsInternal.Count) + { + // Developer did not assign a DisplayIndex or picked a large number. + // Choose the Index as the DisplayIndex. + insertedColumn.DisplayIndexWithFiller = insertedColumn.Index; + } + + try + { + InDisplayIndexAdjustments = true; + + // The DisplayIndex of columns greater than the inserted column need to be incremented, + // as do the DisplayIndexMap values of modified column Indexes + DataGridColumn column; + for (int displayIndex = 0; displayIndex < this.ColumnsInternal.DisplayIndexMap.Count; displayIndex++) + { + if (this.ColumnsInternal.DisplayIndexMap[displayIndex] >= insertedColumn.Index) + { + this.ColumnsInternal.DisplayIndexMap[displayIndex]++; + } + + if (displayIndex >= insertedColumn.DisplayIndexWithFiller) + { + column = this.ColumnsInternal.GetColumnAtDisplayIndex(displayIndex); + column.DisplayIndexWithFiller++; + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + + this.ColumnsInternal.DisplayIndexMap.Insert(insertedColumn.DisplayIndexWithFiller, insertedColumn.Index); + +#if DEBUG + Debug.Assert(this.ColumnsInternal.Debug_VerifyColumnDisplayIndexes(), "Expected ColumnsInternal.Debug_VerifyColumnDisplayIndexes() is true."); +#endif + + // Now raise all the OnColumnDisplayIndexChanged events + FlushDisplayIndexChanged(true /*raiseEvent*/); + } + finally + { + InDisplayIndexAdjustments = false; + FlushDisplayIndexChanged(false /*raiseEvent*/); + } + } + + private void CorrectColumnFrozenStates() + { + int index = 0; + double frozenColumnWidth = 0; + double oldFrozenColumnWidth = 0; + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns()) + { + if (column.IsFrozen) + { + oldFrozenColumnWidth += column.ActualWidth; + } + + column.IsFrozen = index < this.FrozenColumnCountWithFiller; + if (column.IsFrozen) + { + frozenColumnWidth += column.ActualWidth; + } + + index++; + } + + if (this.HorizontalOffset > Math.Max(0, frozenColumnWidth - oldFrozenColumnWidth)) + { + UpdateHorizontalOffset(this.HorizontalOffset - frozenColumnWidth + oldFrozenColumnWidth); + } + else + { + UpdateHorizontalOffset(0); + } + } + + private void CorrectColumnIndexesAfterDeletion(DataGridColumn deletedColumn) + { + Debug.Assert(deletedColumn != null, "Expected non-null deletedColumn."); + for (int columnIndex = deletedColumn.Index; columnIndex < this.ColumnsItemsInternal.Count; columnIndex++) + { + this.ColumnsItemsInternal[columnIndex].Index = this.ColumnsItemsInternal[columnIndex].Index - 1; + Debug.Assert(this.ColumnsItemsInternal[columnIndex].Index == columnIndex, "Expected ColumnsItemsInternal[columnIndex].Index equals columnIndex."); + } + } + + private void CorrectColumnIndexesAfterInsertion(DataGridColumn insertedColumn, int insertionCount) + { + Debug.Assert(insertedColumn != null, "Expected non-null insertedColumn."); + Debug.Assert(insertionCount > 0, "Expected strictly positive insertionCount."); + for (int columnIndex = insertedColumn.Index + insertionCount; columnIndex < this.ColumnsItemsInternal.Count; columnIndex++) + { + this.ColumnsItemsInternal[columnIndex].Index = columnIndex; + } + } + + /// + /// Decreases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total + /// width is decreased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The adjustment stops when + /// the column's target width has been met. + /// + /// Starting column DisplayIndex. + /// The target width of the column (in pixels). + /// Amount to decrease (in pixels). + /// Whether or not to reverse the order in which the columns are traversed. + /// Whether or not to adjust widths of columns that do not yet have their initial desired width. + /// The remaining amount of adjustment. + private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) + { + if (DoubleUtil.GreaterThanOrClose(amount, 0)) + { + return amount; + } + + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns( + reverse, + column => + column.IsVisible && + column.Width.UnitType != DataGridLengthUnitType.Star && + column.DisplayIndex >= displayIndex && + column.ActualCanUserResize && + (affectNewColumns || column.IsInitialDesiredWidthDetermined))) + { + amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); + if (DoubleUtil.IsZero(amount)) + { + break; + } + } + + return amount; + } + + private void FlushDisplayIndexChanged(bool raiseEvent) + { + foreach (DataGridColumn column in this.ColumnsItemsInternal) + { + if (column.DisplayIndexHasChanged) + { + column.DisplayIndexHasChanged = false; + if (raiseEvent) + { + Debug.Assert(column != this.ColumnsInternal.RowGroupSpacerColumn, "Expected column other than ColumnsInternal.RowGroupSpacerColumn."); + OnColumnDisplayIndexChanged(column); + } + } + } + } + + private void GenerateColumnsFromProperties() + { + // Auto-generated Columns are added at the end so the user columns appear first + if (this.DataConnection.DataProperties != null && this.DataConnection.DataProperties.Length > 0) + { + List> columnOrderPairs = new List>(); + + // Generate the columns + foreach (PropertyInfo propertyInfo in this.DataConnection.DataProperties) + { + string columnHeader = propertyInfo.Name; + int columnOrder = DATAGRID_defaultColumnDisplayOrder; + + // Check if DisplayAttribute is defined on the property + DisplayAttribute displayAttribute = propertyInfo.GetCustomAttributes().OfType().FirstOrDefault(); + if (displayAttribute != null) + { + bool? autoGenerateField = displayAttribute.GetAutoGenerateField(); + if (autoGenerateField.HasValue && autoGenerateField.Value == false) + { + // Abort column generation because we aren't supposed to auto-generate this field + continue; + } + + string header = displayAttribute.GetShortName(); + if (header != null) + { + columnHeader = header; + } + + int? order = displayAttribute.GetOrder(); + if (order.HasValue) + { + columnOrder = order.Value; + } + } + + // Generate a single column and determine its relative order + int insertIndex = 0; + if (columnOrder == int.MaxValue) + { + insertIndex = columnOrderPairs.Count; + } + else + { + foreach (KeyValuePair columnOrderPair in columnOrderPairs) + { + if (columnOrderPair.Key > columnOrder) + { + break; + } + + insertIndex++; + } + } + + DataGridAutoGeneratingColumnEventArgs columnArgs = GenerateColumn(propertyInfo.PropertyType, propertyInfo.Name, columnHeader); + columnOrderPairs.Insert(insertIndex, new KeyValuePair(columnOrder, columnArgs)); + } + + // Add the columns to the DataGrid in the correct order + foreach (KeyValuePair columnOrderPair in columnOrderPairs) + { + AddGeneratedColumn(columnOrderPair.Value); + } + } + else if (this.DataConnection.DataIsPrimitive) + { + AddGeneratedColumn(GenerateColumn(this.DataConnection.DataType, string.Empty, this.DataConnection.DataType.Name)); + } + } + + private bool GetColumnEffectiveReadOnlyState(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + + return this.IsReadOnly || dataGridColumn.IsReadOnly || dataGridColumn is DataGridFillerColumn; + } + + /// + /// Returns the absolute coordinate of the left edge of the given column (including + /// the potential gridline - that is the left edge of the gridline is returned). Note that + /// the column does not need to be in the display area. + /// + /// Absolute coordinate of the left edge of the given column. + private double GetColumnXFromIndex(int index) + { + Debug.Assert(index < this.ColumnsItemsInternal.Count, "Expected index smaller than this.ColumnsItemsInternal.Count."); + Debug.Assert(this.ColumnsItemsInternal[index].IsVisible, "Expected ColumnsItemsInternal[index].IsVisible is true."); + + double x = 0; + foreach (DataGridColumn column in this.ColumnsInternal.GetVisibleColumns()) + { + if (index == column.Index) + { + break; + } + + x += GetEdgedColumnWidth(column); + } + + return x; + } + + private double GetNegHorizontalOffsetFromHorizontalOffset(double horizontalOffset) + { + foreach (DataGridColumn column in this.ColumnsInternal.GetVisibleScrollingColumns()) + { + if (GetEdgedColumnWidth(column) > horizontalOffset) + { + break; + } + + horizontalOffset -= GetEdgedColumnWidth(column); + } + + return horizontalOffset; + } + + /// + /// Increases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total + /// width is increased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The adjustment stops when + /// the column's target width has been met. + /// + /// Starting column DisplayIndex. + /// The target width of the column (in pixels). + /// Amount to increase (in pixels). + /// Whether or not to reverse the order in which the columns are traversed. + /// Whether or not to adjust widths of columns that do not yet have their initial desired width. + /// The remaining amount of adjustment. + private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) + { + if (DoubleUtil.LessThanOrClose(amount, 0)) + { + return amount; + } + + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns( + reverse, + column => + column.IsVisible && + column.Width.UnitType != DataGridLengthUnitType.Star && + column.DisplayIndex >= displayIndex && + column.ActualCanUserResize && + (affectNewColumns || column.IsInitialDesiredWidthDetermined))) + { + amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); + if (DoubleUtil.IsZero(amount)) + { + break; + } + } + + return amount; + } + + private void InsertDisplayedColumnHeader(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null, "Expected non-null dataGridColumn."); + if (_columnHeadersPresenter != null) + { + dataGridColumn.HeaderCell.Visibility = dataGridColumn.Visibility; + Debug.Assert(!_columnHeadersPresenter.Children.Contains(dataGridColumn.HeaderCell), "Expected dataGridColumn.HeaderCell not contained in _columnHeadersPresenter.Children."); + _columnHeadersPresenter.Children.Insert(dataGridColumn.DisplayIndexWithFiller, dataGridColumn.HeaderCell); + } + } + + private void RemoveAutoGeneratedColumns() + { + int index = 0; + _autoGeneratingColumnOperationCount++; + try + { + while (index < this.ColumnsInternal.Count) + { + // Skip over the user columns + while (index < this.ColumnsInternal.Count && !this.ColumnsInternal[index].IsAutoGenerated) + { + index++; + } + + // Remove the auto-generated columns + while (index < this.ColumnsInternal.Count && this.ColumnsInternal[index].IsAutoGenerated) + { + this.ColumnsInternal.RemoveAt(index); + } + } + + this.ColumnsInternal.AutogeneratedColumnCount = 0; + } + finally + { + _autoGeneratingColumnOperationCount--; + } + } + + private bool ScrollColumnIntoView(int columnIndex) + { + Debug.Assert(columnIndex >= 0, "Expected positive columnIndex."); + Debug.Assert(columnIndex < this.ColumnsItemsInternal.Count, "Expected columnIndex smaller than this.ColumnsItemsInternal.Count."); + + if (this.DisplayData.FirstDisplayedScrollingCol != -1 && + !this.ColumnsItemsInternal[columnIndex].IsFrozen && + (columnIndex != this.DisplayData.FirstDisplayedScrollingCol || _negHorizontalOffset > 0)) + { + int columnsToScroll; + if (this.ColumnsInternal.DisplayInOrder(columnIndex, this.DisplayData.FirstDisplayedScrollingCol)) + { + columnsToScroll = this.ColumnsInternal.GetColumnCount(true /*isVisible*/, false /*isFrozen*/, columnIndex, this.DisplayData.FirstDisplayedScrollingCol); + if (_negHorizontalOffset > 0) + { + columnsToScroll++; + } + + ScrollColumns(-columnsToScroll); + } + else if (columnIndex == this.DisplayData.FirstDisplayedScrollingCol && _negHorizontalOffset > 0) + { + ScrollColumns(-1); + } + else if (this.DisplayData.LastTotallyDisplayedScrollingCol == -1 || + (this.DisplayData.LastTotallyDisplayedScrollingCol != columnIndex && + this.ColumnsInternal.DisplayInOrder(this.DisplayData.LastTotallyDisplayedScrollingCol, columnIndex))) + { + double xColumnLeftEdge = GetColumnXFromIndex(columnIndex); + double xColumnRightEdge = xColumnLeftEdge + GetEdgedColumnWidth(this.ColumnsItemsInternal[columnIndex]); + double change = xColumnRightEdge - this.HorizontalOffset - this.CellsWidth; + double widthRemaining = change; + + DataGridColumn newFirstDisplayedScrollingCol = this.ColumnsItemsInternal[this.DisplayData.FirstDisplayedScrollingCol]; + DataGridColumn nextColumn = this.ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol); + double newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol) - _negHorizontalOffset; + while (nextColumn != null && widthRemaining >= newColumnWidth) + { + widthRemaining -= newColumnWidth; + newFirstDisplayedScrollingCol = nextColumn; + newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol); + nextColumn = this.ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol); + _negHorizontalOffset = 0; + } + + _negHorizontalOffset += widthRemaining; + this.DisplayData.LastTotallyDisplayedScrollingCol = columnIndex; + if (newFirstDisplayedScrollingCol.Index == columnIndex) + { + _negHorizontalOffset = 0; + double frozenColumnWidth = this.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); + + // If the entire column cannot be displayed, we want to start showing it from its LeftEdge. + if (newColumnWidth > (this.CellsWidth - frozenColumnWidth)) + { + this.DisplayData.LastTotallyDisplayedScrollingCol = -1; + change = xColumnLeftEdge - this.HorizontalOffset - frozenColumnWidth; + } + } + + this.DisplayData.FirstDisplayedScrollingCol = newFirstDisplayedScrollingCol.Index; + + // At this point DisplayData.FirstDisplayedScrollingColumn and LastDisplayedScrollingColumn should be correct. + if (change != 0) + { + UpdateHorizontalOffset(this.HorizontalOffset + change); + } + } + } + + return true; + } + + private void ScrollColumns(int columns) + { + DataGridColumn newFirstVisibleScrollingCol = null; + DataGridColumn dataGridColumnTmp; + int colCount = 0; + if (columns > 0) + { + if (this.DisplayData.LastTotallyDisplayedScrollingCol >= 0) + { + dataGridColumnTmp = this.ColumnsItemsInternal[this.DisplayData.LastTotallyDisplayedScrollingCol]; + while (colCount < columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = this.ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp); + colCount++; + } + + if (dataGridColumnTmp == null) + { + // no more column to display on the right of the last totally seen column + return; + } + } + + Debug.Assert(this.DisplayData.FirstDisplayedScrollingCol >= 0, "Expected positive DisplayData.FirstDisplayedScrollingCol."); + dataGridColumnTmp = this.ColumnsItemsInternal[this.DisplayData.FirstDisplayedScrollingCol]; + colCount = 0; + while (colCount < columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = this.ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp); + colCount++; + } + + newFirstVisibleScrollingCol = dataGridColumnTmp; + } + + if (columns < 0) + { + Debug.Assert(this.DisplayData.FirstDisplayedScrollingCol >= 0, "Expected positive DisplayData.FirstDisplayedScrollingCol."); + dataGridColumnTmp = this.ColumnsItemsInternal[this.DisplayData.FirstDisplayedScrollingCol]; + if (_negHorizontalOffset > 0) + { + colCount++; + } + + while (colCount < -columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = this.ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumnTmp); + colCount++; + } + + newFirstVisibleScrollingCol = dataGridColumnTmp; + if (newFirstVisibleScrollingCol == null) + { + if (_negHorizontalOffset == 0) + { + // no more column to display on the left of the first seen column + return; + } + else + { + newFirstVisibleScrollingCol = this.ColumnsItemsInternal[this.DisplayData.FirstDisplayedScrollingCol]; + } + } + } + + double newColOffset = 0; + foreach (DataGridColumn dataGridColumn in this.ColumnsInternal.GetVisibleScrollingColumns()) + { + if (dataGridColumn == newFirstVisibleScrollingCol) + { + break; + } + + newColOffset += GetEdgedColumnWidth(dataGridColumn); + } + + UpdateHorizontalOffset(newColOffset); + } + + private void UpdateDisplayedColumns() + { + this.DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + ComputeDisplayedColumns(); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridComboBoxColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridComboBoxColumn.cs new file mode 100644 index 0000000..6165e31 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridComboBoxColumn.cs @@ -0,0 +1,738 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Text; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column that hosts textual content in its cells. In edit mode data can be changed to a value from a collection hosted in a ComboBox. + /// + [StyleTypedProperty(Property = "ElementStyle", StyleTargetType = typeof(TextBlock))] + [StyleTypedProperty(Property = "EditingElementStyle", StyleTargetType = typeof(ComboBox))] + public class DataGridComboBoxColumn : DataGridBoundColumn + { + private const string DATAGRIDCOMBOBOXCOLUMN_fontFamilyName = "FontFamily"; + private const string DATAGRIDCOMBOBOXCOLUMN_fontSizeName = "FontSize"; + private const string DATAGRIDCOMBOBOXCOLUMN_fontStyleName = "FontStyle"; + private const string DATAGRIDCOMBOBOXCOLUMN_fontWeightName = "FontWeight"; + private const string DATAGRIDCOMBOBOXCOLUMN_foregroundName = "Foreground"; + private const string DATAGRIDCOMBOBOXCOLUMN_itemsSourceName = "ItemsSource"; + private const string DATAGRIDCOMBOBOXCOLUMN_displayMemberPathName = "DisplayMemberPath"; + private const double DATAGRIDCOMBOBOXCOLUMN_leftMargin = 12.0; + private const double DATAGRIDCOMBOBOXCOLUMN_rightMargin = 12.0; + + private double? _fontSize; + private DataGrid _owningGrid; + private FontStyle? _fontStyle; + private FontWeight? _fontWeight; + private Brush _foreground; + private HashSet _notifyingDataItems; + + /// + /// Identifies the ItemsSource dependency property. + /// + public static readonly DependencyProperty ItemsSourceProperty = + DependencyProperty.Register( + DATAGRIDCOMBOBOXCOLUMN_itemsSourceName, + typeof(IEnumerable), + typeof(DataGridComboBoxColumn), + new PropertyMetadata(default(IEnumerable), OnItemSourcePropertyChanged)); + + /// + /// Gets or sets a collection that is used to generate the content of the ComboBox while in editing mode. + /// + public IEnumerable ItemsSource + { + get { return (IEnumerable)GetValue(ItemsSourceProperty); } + set { SetValue(ItemsSourceProperty, value); } + } + + private static void OnItemSourcePropertyChanged(DependencyObject comboBoxDependencyObject, DependencyPropertyChangedEventArgs e) + { + var comboColumn = comboBoxDependencyObject as DataGridComboBoxColumn; + comboColumn.NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_itemsSourceName); + } + + /// + /// Identifies the DisplayMemberPath dependency property. + /// + public static readonly DependencyProperty DisplayMemberPathProperty = + DependencyProperty.Register( + DATAGRIDCOMBOBOXCOLUMN_displayMemberPathName, + typeof(string), + typeof(DataGridComboBoxColumn), + new PropertyMetadata(default(string))); + + /// + /// Gets or sets the name or path of the property that is displayed in the ComboBox. + /// + public string DisplayMemberPath + { + get { return (string)GetValue(DisplayMemberPathProperty); } + set { SetValue(DisplayMemberPathProperty, value); } + } + + /// + /// Gets or sets the font name. + /// + public FontFamily FontFamily + { + get { return (FontFamily)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Identifies the FontFamily dependency property. + /// + public static readonly DependencyProperty FontFamilyProperty = + DependencyProperty.Register( + DATAGRIDCOMBOBOXCOLUMN_fontFamilyName, + typeof(FontFamily), + typeof(DataGridComboBoxColumn), + new PropertyMetadata(null, OnFontFamilyPropertyChanged)); + + private static void OnFontFamilyPropertyChanged(DependencyObject comboBoxColumnDependencyObject, DependencyPropertyChangedEventArgs e) + { + var comboColumn = comboBoxColumnDependencyObject as DataGridComboBoxColumn; + comboColumn.NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_fontFamilyName); + } + + /// + /// Gets or sets the font size. + /// + // Use DefaultValue here so undo in the Designer will set this to NaN + [DefaultValue(double.NaN)] + public double FontSize + { + get + { + return _fontSize ?? double.NaN; + } + + set + { + if (_fontSize != value) + { + _fontSize = value; + NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_fontSizeName); + } + } + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get + { + return _fontStyle ?? FontStyle.Normal; + } + + set + { + if (_fontStyle != value) + { + _fontStyle = value; + NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_fontStyleName); + } + } + } + + /// + /// Gets or sets the font weight or thickness. + /// + public FontWeight FontWeight + { + get + { + return _fontWeight ?? FontWeights.Normal; + } + + set + { + if (!_fontWeight.HasValue || _fontWeight.Value.Weight != value.Weight) + { + _fontWeight = value; + NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_fontWeightName); + } + } + } + + /// + /// Gets or sets a brush that describes the foreground of the column cells. + /// + public Brush Foreground + { + get + { + return _foreground; + } + + set + { + if (_foreground != value) + { + _foreground = value; + NotifyPropertyChanged(DATAGRIDCOMBOBOXCOLUMN_foregroundName); + } + } + } + + /// + /// Gets a control that is bound to the column's ItemsSource collection. + /// + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// A new control that is bound to the column's ItemsSource collection. + protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) + { + EnsureColumnBinding(dataItem); + + EnsureDisplayMemberPathExists(); + + EnsureItemsSourceBinding(); + + EnsureColumnTypeAgreement(dataItem); + + var comboBox = new ComboBox + { + Margin = default(Thickness), + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center + }; + + if (dataItem != null) + { + var value = TypeHelper.GetNestedPropertyValue(dataItem, Binding.Path.Path); + + var selection = !string.IsNullOrEmpty(DisplayMemberPath) + ? ItemsSource?.Cast().FirstOrDefault(x => TypeHelper.GetNestedPropertyValue(x, Binding.GetBindingPropertyName()).Equals(value)) + : ItemsSource?.Cast().FirstOrDefault(x => x.Equals(value)); + + comboBox.SelectedItem = selection; + } + + var itemsSourceBinding = new Binding + { + Source = this, + Path = new PropertyPath(DATAGRIDCOMBOBOXCOLUMN_itemsSourceName) + }; + + var displayMemberPathBinding = new Binding + { + Source = this, + Path = new PropertyPath(DATAGRIDCOMBOBOXCOLUMN_displayMemberPathName) + }; + + comboBox.SetBinding(ComboBox.ItemsSourceProperty, itemsSourceBinding); + + comboBox.SetBinding(ComboBox.DisplayMemberPathProperty, displayMemberPathBinding); + + if (DependencyProperty.UnsetValue != ReadLocalValue(DataGridComboBoxColumn.FontFamilyProperty)) + { + comboBox.FontFamily = FontFamily; + } + + if (_fontSize.HasValue) + { + comboBox.FontSize = _fontSize.Value; + } + + if (_fontStyle.HasValue) + { + comboBox.FontStyle = _fontStyle.Value; + } + + if (_fontWeight.HasValue) + { + comboBox.FontWeight = _fontWeight.Value; + } + + RefreshForeground(comboBox, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + + comboBox.SelectionChanged += (sender, args) => + { + var item = args.AddedItems.FirstOrDefault(); + + if (item != null) + { + var newValue = !string.IsNullOrEmpty(DisplayMemberPath) + ? item.GetType().GetProperty(Binding.GetBindingPropertyName())?.GetValue(item) + : item; + + TypeHelper.SetNestedPropertyValue(ref dataItem, newValue, Binding.Path.Path); + } + }; + + return comboBox; + } + + /// + /// Gets a read-only element that is bound to the column's property value. + /// + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// A new, read-only element that is bound to the column's property value. + protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) + { + EnsureColumnBinding(dataItem); + EnsureColumnTypeAgreement(dataItem); + EnsureDisplayMemberPathExists(); + EnsureItemsSourceBinding(); + + var textBlockElement = new TextBlock + { + Margin = new Thickness(DATAGRIDCOMBOBOXCOLUMN_leftMargin, 0.0, DATAGRIDCOMBOBOXCOLUMN_rightMargin, 0.0), + VerticalAlignment = VerticalAlignment.Center + }; + + if (DependencyProperty.UnsetValue != ReadLocalValue(DataGridComboBoxColumn.FontFamilyProperty)) + { + textBlockElement.FontFamily = FontFamily; + } + + if (_fontSize.HasValue) + { + textBlockElement.FontSize = _fontSize.Value; + } + + if (_fontStyle.HasValue) + { + textBlockElement.FontStyle = _fontStyle.Value; + } + + if (_fontWeight.HasValue) + { + textBlockElement.FontWeight = _fontWeight.Value; + } + + RefreshForeground(textBlockElement, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + + if (Binding != null && EnsureOwningGrid()) + { + if (string.IsNullOrEmpty(DisplayMemberPath)) + { + textBlockElement.SetBinding(TextBlock.TextProperty, Binding); + } + else + { + textBlockElement.Text = GetDisplayValue(dataItem); + + HookDataItemPropertyChanged(dataItem); + } + } + + return textBlockElement; + } + + /// + /// Causes the column cell being edited to revert to the specified value. + /// + /// The element that the column displays for a cell in editing mode. + /// The previous, unedited value in the cell being edited. + protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue) + { + var comboBox = editingElement as ComboBox; + + if (comboBox != null) + { + if (uneditedValue != null) + { + var property = uneditedValue.GetType().GetNestedProperty(Binding.GetBindingPropertyName()); + + if (property == null) + { + comboBox.SelectedItem = uneditedValue; + } + else + { + var value = TypeHelper.GetNestedPropertyValue(uneditedValue, Binding.GetBindingPropertyName()); + var selection = ItemsSource?.Cast().FirstOrDefault(x => TypeHelper.GetNestedPropertyValue(x, Binding.GetBindingPropertyName()).Equals(value)); + + comboBox.SelectedItem = selection; + } + } + else + { + comboBox.SelectedItem = null; + } + } + } + + /// + /// Called when the cell in the column enters editing mode. + /// + /// The element that the column displays for a cell in editing mode. + /// Information about the user gesture that is causing a cell to enter editing mode. + /// The unedited value. + protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + return (editingElement as ComboBox)?.SelectedItem; + } + + /// + /// Called by the DataGrid control when this column asks for its elements to be updated, because a property changed. + /// + protected internal override void RefreshCellContent(FrameworkElement element, Brush computedRowForeground, string propertyName) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + var comboBox = element as ComboBox; + if (comboBox == null) + { + var textBlock = element as TextBlock; + if (textBlock == null) + { + throw DataGridError.DataGrid.ValueIsNotAnInstanceOfEitherOr(nameof(element), typeof(ComboBox), typeof(TextBlock)); + } + + if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontFamilyName) + { + textBlock.FontFamily = FontFamily; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontSizeName) + { + SetTextFontSize(textBlock, TextBlock.FontSizeProperty); + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontStyleName) + { + textBlock.FontStyle = FontStyle; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontWeightName) + { + textBlock.FontWeight = FontWeight; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_foregroundName) + { + RefreshForeground(textBlock, computedRowForeground); + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_itemsSourceName) + { + OwningGrid.OnColumnBindingChanged(this); + } + else + { + if (FontFamily != null) + { + textBlock.FontFamily = FontFamily; + } + + SetTextFontSize(textBlock, TextBlock.FontSizeProperty); + textBlock.FontStyle = FontStyle; + textBlock.FontWeight = FontWeight; + RefreshForeground(textBlock, computedRowForeground); + } + + return; + } + + if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontFamilyName) + { + comboBox.FontFamily = FontFamily; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontSizeName) + { + SetTextFontSize(comboBox, ComboBox.FontSizeProperty); + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontStyleName) + { + comboBox.FontStyle = FontStyle; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_fontWeightName) + { + comboBox.FontWeight = FontWeight; + } + else if (propertyName == DATAGRIDCOMBOBOXCOLUMN_foregroundName) + { + RefreshForeground(comboBox, computedRowForeground); + } + else + { + if (FontFamily != null) + { + comboBox.FontFamily = FontFamily; + } + + SetTextFontSize(comboBox, ComboBox.FontSizeProperty); + comboBox.FontStyle = FontStyle; + comboBox.FontWeight = FontWeight; + RefreshForeground(comboBox, computedRowForeground); + } + } + + /// + /// Called when the computed foreground of a row changed. + /// + protected internal override void RefreshForeground(FrameworkElement element, Brush computedRowForeground) + { + if (element is ComboBox comboBox) + { + RefreshForeground(comboBox, computedRowForeground); + } + else if (element is TextBlock textBlock) + { + RefreshForeground(textBlock, computedRowForeground); + } + } + + private void RefreshForeground(ComboBox comboBox, Brush computedRowForeground) + { + if (Foreground == null) + { + if (computedRowForeground != null) + { + comboBox.Foreground = computedRowForeground; + } + } + else + { + comboBox.Foreground = Foreground; + } + } + + private void RefreshForeground(TextBlock textBlock, Brush computedRowForeground) + { + if (Foreground == null) + { + if (computedRowForeground != null) + { + textBlock.Foreground = computedRowForeground; + } + } + else + { + textBlock.Foreground = Foreground; + } + } + + private void SetTextFontSize(DependencyObject textElement, DependencyProperty fontSizeProperty) + { + double newFontSize = FontSize; + if (double.IsNaN(newFontSize)) + { + textElement.ClearValue(fontSizeProperty); + } + else + { + textElement.SetValue(fontSizeProperty, newFontSize); + } + } + + private bool EnsureOwningGrid() + { + if (OwningGrid != null) + { + if (OwningGrid != _owningGrid) + { + _owningGrid = OwningGrid; + _owningGrid.Columns.CollectionChanged += new NotifyCollectionChangedEventHandler(Columns_CollectionChanged); + _owningGrid.LoadingRow += OwningGrid_LoadingRow; + _owningGrid.UnloadingRow += OwningGrid_UnloadingRow; + _owningGrid.CellEditEnded += OwningGrid_CellEditEnded; + } + + return true; + } + + return false; + } + + private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e) + { + HookDataItemPropertyChanged(e.Row.DataContext); + SetDisplayMemberPathValue(e.Row); + } + + private void OwningGrid_UnloadingRow(object sender, DataGridRowEventArgs e) + { + UnhookDataItemPropertyChanged(e.Row.DataContext); + } + + private void OwningGrid_CellEditEnded(object sender, DataGridCellEditEndedEventArgs e) + { + SetDisplayMemberPathValue(e.Row); + } + + private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (OwningGrid == null && _owningGrid != null) + { + _notifyingDataItems?.Clear(); + _owningGrid.Columns.CollectionChanged -= new NotifyCollectionChangedEventHandler(Columns_CollectionChanged); + _owningGrid.LoadingRow -= OwningGrid_LoadingRow; + _owningGrid.UnloadingRow -= this.OwningGrid_UnloadingRow; + _owningGrid.CellEditEnded -= OwningGrid_CellEditEnded; + _owningGrid = null; + } + } + + private void SetDisplayMemberPathValue(DataGridRow row) + { + if (OwningGrid != null && !string.IsNullOrEmpty(DisplayMemberPath)) + { + var textBlock = GetCellContent(row) as TextBlock; + if (textBlock != null) + { + var displayValue = GetDisplayValue(row.DataContext); + + textBlock.Text = displayValue; + } + } + } + + private string GetDisplayValue(object dataItem) + { + if (Binding?.Path != null && dataItem != null) + { + var value = TypeHelper.GetNestedPropertyValue(dataItem, Binding.Path.Path); + + var item = ItemsSource?.Cast().FirstOrDefault(x => TypeHelper.GetNestedPropertyValue(x, Binding.GetBindingPropertyName()).Equals(value)); + + var displayValue = item?.GetType().GetProperty(DisplayMemberPath).GetValue(item) ?? string.Empty; + + return displayValue as string ?? displayValue.ToString(); + } + + return string.Empty; + } + + private void EnsureColumnBinding(object dataItem) + { + if (Binding?.Path == null) + { + if (!string.IsNullOrEmpty(Header as string)) + { + throw DataGridError.DataGridComboBoxColumn.UnsetBinding(Header as string); + } + + throw DataGridError.DataGridComboBoxColumn.UnsetBinding(GetType()); + } + + var property = dataItem?.GetType().GetNestedProperty(Binding?.Path?.Path); + + if (property == null && dataItem != null) + { + throw DataGridError.DataGridComboBoxColumn.UnknownBindingPath(Binding, dataItem?.GetType()); + } + } + + private void EnsureColumnTypeAgreement(object dataItem) + { + if (string.IsNullOrEmpty(DisplayMemberPath)) + { + var itemsSourceType = ItemsSource?.GetType().GetEnumerableItemType(); + var dataItemType = dataItem?.GetType().GetNestedPropertyType(Binding?.Path?.Path); + + if (dataItemType != null && itemsSourceType != null && itemsSourceType != dataItemType) + { + throw DataGridError.DataGridComboBoxColumn.BindingTypeMismatch(dataItemType, itemsSourceType); + } + } + } + + private void EnsureDisplayMemberPathExists() + { + if (!string.IsNullOrEmpty(DisplayMemberPath)) + { + var type = ItemsSource?.GetItemType(); + + if (ItemsSource != null && !type.GetProperties().Any(x => x.Name.Equals(DisplayMemberPath))) + { + throw DataGridError.DataGridComboBoxColumn.UnknownDisplayMemberPath(DisplayMemberPath, type); + } + } + } + + private void EnsureItemsSourceBinding() + { + if (!string.IsNullOrEmpty(DisplayMemberPath) && ItemsSource != null) + { + var item = ItemsSource.Cast().FirstOrDefault(); + + if (item != null && !item.GetType().GetProperties().Any(y => y.Name.Equals(Binding.GetBindingPropertyName()))) + { + throw DataGridError.DataGridComboBoxColumn.UnknownItemsSourcePath(Binding); + } + } + } + + private void HookDataItemPropertyChanged(object dataItem) + { + if (Binding.Mode == BindingMode.OneTime) + { + return; + } + + var notifyingDataItem = dataItem as INotifyPropertyChanged; + + if (notifyingDataItem == null) + { + return; + } + + if (_notifyingDataItems == null) + { + _notifyingDataItems = new HashSet(); + } + + if (!_notifyingDataItems.Contains(dataItem)) + { + notifyingDataItem.PropertyChanged += DataItem_PropertyChanged; + _notifyingDataItems.Add(dataItem); + } + } + + private void UnhookDataItemPropertyChanged(object dataItem) + { + if (_notifyingDataItems == null) + { + return; + } + + var notifyingDataItem = dataItem as INotifyPropertyChanged; + + if (notifyingDataItem == null) + { + return; + } + + if (_notifyingDataItems.Contains(dataItem)) + { + notifyingDataItem.PropertyChanged -= DataItem_PropertyChanged; + _notifyingDataItems.Remove(dataItem); + } + } + + private void DataItem_PropertyChanged(object dataItem, PropertyChangedEventArgs e) + { + if (this.OwningGrid != null && Binding?.Path != null && this.Binding.Path.Path == e.PropertyName) + { + var dataGridRow = OwningGrid.GetRowFromItem(dataItem); + + if (dataGridRow != null && this.GetCellContent(dataGridRow) is TextBlock textBlockElement) + { + textBlockElement.Text = GetDisplayValue(dataItem); + } + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDataConnection.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDataConnection.cs new file mode 100644 index 0000000..d584336 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDataConnection.cs @@ -0,0 +1,1067 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Microsoft.Toolkit.Uwp.UI.Data.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridDataConnection + { + private int _backupSlotForCurrentChanged; + private int _columnForCurrentChanged; + private PropertyInfo[] _dataProperties; + private IEnumerable _dataSource; + private Type _dataType; + private bool _expectingCurrentChanged; + private ISupportIncrementalLoading _incrementalItemsSource; + private object _itemToSelectOnCurrentChanged; + private IAsyncOperation _loadingOperation; + private DataGrid _owner; + private bool _scrollForCurrentChanged; + private DataGridSelectionAction _selectionActionForCurrentChanged; + private WeakEventListener _weakCollectionChangedListener; + private WeakEventListener _weakVectorChangedListener; + private WeakEventListener _weakCurrentChangingListener; + private WeakEventListener _weakCurrentChangedListener; + private WeakEventListener _weakIncrementalItemsSourcePropertyChangedListener; + +#if FEATURE_ICOLLECTIONVIEW_SORT + private WeakEventListener _weakSortDescriptionsCollectionChangedListener; +#endif + + public DataGridDataConnection(DataGrid owner) + { + _owner = owner; + } + + public bool AllowEdit + { + get + { + if (this.List == null) + { + return true; + } + else + { + return !this.List.IsReadOnly; + } + } + } + + /// + /// Gets a value indicating whether the collection view says it can sort. + /// + public bool AllowSort + { + get + { + if (this.CollectionView == null) + { + return false; + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (this.EditableCollectionView != null && (this.EditableCollectionView.IsAddingNew || this.EditableCollectionView.IsEditingItem)) + { + return false; + } +#endif + +#if FEATURE_ICOLLECTIONVIEW_SORT + return this.CollectionView.CanSort; +#else + return false; +#endif + } + } + + public bool CanCancelEdit + { + get + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + return this.EditableCollectionView != null && this.EditableCollectionView.CanCancelEdit; +#else + return false; +#endif + } + } + + public ICollectionView CollectionView + { + get + { + return this.DataSource as ICollectionView; + } + } + + public int Count + { + get + { + IList list = this.List; + if (list != null) + { + return list.Count; + } + +#if FEATURE_PAGEDCOLLECTIONVIEW + PagedCollectionView collectionView = this.DataSource as PagedCollectionView; + if (collectionView != null) + { + return collectionView.Count; + } +#endif + + int count = 0; + IEnumerable enumerable = this.DataSource; + if (enumerable != null) + { + IEnumerator enumerator = enumerable.GetEnumerator(); + if (enumerator != null) + { + while (enumerator.MoveNext()) + { + count++; + } + } + } + + return count; + } + } + + public bool DataIsPrimitive + { + get + { + return DataTypeIsPrimitive(this.DataType); + } + } + + public PropertyInfo[] DataProperties + { + get + { + if (_dataProperties == null) + { + UpdateDataProperties(); + } + + return _dataProperties; + } + } + + public IEnumerable DataSource + { + get + { + return _dataSource; + } + + set + { + _dataSource = value; + + // Because the DataSource is changing, we need to reset our cached values for DataType and DataProperties, + // which are dependent on the current DataSource + _dataType = null; + UpdateDataProperties(); + UpdateIncrementalItemsSource(); + } + } + + public Type DataType + { + get + { + // We need to use the raw ItemsSource as opposed to DataSource because DataSource + // may be the ItemsSource wrapped in a collection view, in which case we wouldn't + // be able to take T to be the type if we're given IEnumerable + if (_dataType == null && _owner.ItemsSource != null) + { + _dataType = _owner.ItemsSource.GetItemType(); + } + + return _dataType; + } + } + + public bool HasMoreItems + { + get { return IsDataSourceIncremental && _incrementalItemsSource.HasMoreItems; } + } + + public bool IsDataSourceIncremental + { + get { return _incrementalItemsSource != null; } + } + + public bool IsLoadingMoreItems + { + get { return _loadingOperation != null; } + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + public IEditableCollectionView EditableCollectionView + { + get + { + return this.DataSource as IEditableCollectionView; + } + } +#endif + + public bool EndingEdit + { + get; + private set; + } + + public bool EventsWired + { + get; + private set; + } + + public bool IsAddingNew + { + get + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + return this.EditableCollectionView != null && this.EditableCollectionView.IsAddingNew; +#else + return false; +#endif + } + } + + private bool IsGrouping + { + get + { + return this.CollectionView != null && +#if FEATURE_ICOLLECTIONVIEW_GROUP + this.CollectionView.CanGroup && +#endif + this.CollectionView.CollectionGroups != null && + this.CollectionView.CollectionGroups.Count > 0; + } + } + + public IList List + { + get + { + return this.DataSource as IList; + } + } + + public int NewItemPlaceholderIndex + { + get + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (this.EditableCollectionView != null && this.EditableCollectionView.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd) + { + return this.Count - 1; + } +#endif + + return -1; + } + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + public NewItemPlaceholderPosition NewItemPlaceholderPosition + { + get + { + if (this.EditableCollectionView != null) + { + return this.EditableCollectionView.NewItemPlaceholderPosition; + } + + return NewItemPlaceholderPosition.None; + } + } +#endif + + public bool ShouldAutoGenerateColumns + { + get + { + return _owner.AutoGenerateColumns + && (_owner.ColumnsInternal.AutogeneratedColumnCount == 0) + && ((this.DataProperties != null && this.DataProperties.Length > 0) || this.DataIsPrimitive); + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + public SortDescriptionCollection SortDescriptions + { + get + { + if (this.CollectionView != null && this.CollectionView.CanSort) + { + return this.CollectionView.SortDescriptions; + } + else + { + return (SortDescriptionCollection)null; + } + } + } +#endif + + public static bool CanEdit(Type type) + { + Debug.Assert(type != null, "Expected non-null type."); + + type = type.GetNonNullableType(); + + return + type.GetTypeInfo().IsEnum + || type == typeof(string) + || type == typeof(char) + || type == typeof(bool) + || type == typeof(byte) + || type == typeof(sbyte) + || type == typeof(float) + || type == typeof(double) + || type == typeof(decimal) + || type == typeof(short) + || type == typeof(int) + || type == typeof(long) + || type == typeof(ushort) + || type == typeof(uint) + || type == typeof(ulong) + || type == typeof(DateTime); + } + + /// + /// Puts the entity into editing mode if possible + /// + /// The entity to edit + /// True if editing was started + public bool BeginEdit(object dataItem) + { + if (dataItem == null) + { + return false; + } + +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView editableCollectionView = this.EditableCollectionView; + if (editableCollectionView != null) + { + if ((editableCollectionView.IsEditingItem && (dataItem == editableCollectionView.CurrentEditItem)) || + (editableCollectionView.IsAddingNew && (dataItem == editableCollectionView.CurrentAddItem))) + { + return true; + } + else + { + editableCollectionView.EditItem(dataItem); + return editableCollectionView.IsEditingItem; + } + } +#endif + + IEditableObject editableDataItem = dataItem as IEditableObject; + if (editableDataItem != null) + { + editableDataItem.BeginEdit(); + return true; + } + + return true; + } + + /// + /// Cancels the current entity editing and exits the editing mode. + /// + /// The entity being edited + /// True if a cancellation operation was invoked. + public bool CancelEdit(object dataItem) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView editableCollectionView = this.EditableCollectionView; + if (editableCollectionView != null) + { + _owner.NoCurrentCellChangeCount++; + this.EndingEdit = true; + try + { + if (editableCollectionView.IsAddingNew && dataItem == editableCollectionView.CurrentAddItem) + { + editableCollectionView.CancelNew(); + return true; + } + else if (editableCollectionView.CanCancelEdit) + { + editableCollectionView.CancelEdit(); + return true; + } + } + finally + { + _owner.NoCurrentCellChangeCount--; + this.EndingEdit = false; + } + + return false; + } +#endif + + IEditableObject editableDataItem = dataItem as IEditableObject; + if (editableDataItem != null) + { + editableDataItem.CancelEdit(); + return true; + } + + return true; + } + + /// + /// Commits the current entity editing and exits the editing mode. + /// + /// The entity being edited + /// True if a commit operation was invoked. + public bool EndEdit(object dataItem) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + IEditableCollectionView editableCollectionView = this.EditableCollectionView; + if (editableCollectionView != null) + { + // IEditableCollectionView.CommitEdit can potentially change currency. If it does, + // we don't want to attempt a second commit inside our CurrentChanging event handler. + _owner.NoCurrentCellChangeCount++; + this.EndingEdit = true; + try + { + if (editableCollectionView.IsAddingNew && dataItem == editableCollectionView.CurrentAddItem) + { + editableCollectionView.CommitNew(); + } + else + { + editableCollectionView.CommitEdit(); + } + } + finally + { + _owner.NoCurrentCellChangeCount--; + this.EndingEdit = false; + } + + return true; + } +#endif + + IEditableObject editableDataItem = dataItem as IEditableObject; + if (editableDataItem != null) + { + editableDataItem.EndEdit(); + } + + return true; + } + + // Assumes index >= 0, returns null if index >= Count + public object GetDataItem(int index) + { + Debug.Assert(index >= 0, "Expected positive index."); + + IList list = this.List; + if (list != null) + { + return (index < list.Count) ? list[index] : null; + } + +#if FEATURE_PAGEDCOLLECTIONVIEW + PagedCollectionView collectionView = this.DataSource as PagedCollectionView; + if (collectionView != null) + { + return (index < collectionView.Count) ? collectionView.GetItemAt(index) : null; + } +#endif + + IEnumerable enumerable = this.DataSource; + if (enumerable != null) + { + IEnumerator enumerator = enumerable.GetEnumerator(); + int i = -1; + while (enumerator.MoveNext() && i < index) + { + i++; + if (i == index) + { + return enumerator.Current; + } + } + } + + return null; + } + + public bool GetPropertyIsReadOnly(string propertyName) + { + if (this.DataType != null) + { + if (!string.IsNullOrEmpty(propertyName)) + { + Type propertyType = this.DataType; + PropertyInfo propertyInfo = null; + List propertyNames = TypeHelper.SplitPropertyPath(propertyName); + for (int i = 0; i < propertyNames.Count; i++) + { + if (propertyType.GetTypeInfo().GetIsReadOnly()) + { + return true; + } + + propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out _); + if (propertyInfo == null || propertyInfo.GetIsReadOnly()) + { + // Either the property doesn't exist or it does exist but is read-only. + return true; + } + + // Check if EditableAttribute is defined on the property and if it indicates uneditable + var editableAttribute = propertyInfo.GetCustomAttributes().OfType().FirstOrDefault(); + if (editableAttribute != null && !editableAttribute.AllowEdit) + { + return true; + } + + propertyType = propertyInfo.PropertyType.GetNonNullableType(); + } + + return propertyInfo == null || !propertyInfo.CanWrite || !this.AllowEdit || !CanEdit(propertyType); + } + else + { + if (this.DataType.GetTypeInfo().GetIsReadOnly()) + { + return true; + } + } + } + + return !this.AllowEdit; + } + + public int IndexOf(object dataItem) + { + IList list = this.List; + if (list != null) + { + return list.IndexOf(dataItem); + } + +#if FEATURE_PAGEDCOLLECTIONVIEW + PagedCollectionView cv = this.DataSource as PagedCollectionView; + if (cv != null) + { + return cv.IndexOf(dataItem); + } +#endif + + IEnumerable enumerable = this.DataSource; + if (enumerable != null && dataItem != null) + { + int index = 0; + foreach (object dataItemTmp in enumerable) + { + if ((dataItem == null && dataItemTmp == null) || + dataItem.Equals(dataItemTmp)) + { + return index; + } + + index++; + } + } + + return -1; + } + + public void LoadMoreItems(uint count) + { + Debug.Assert(_loadingOperation == null, "Expected _loadingOperation == null."); + + _loadingOperation = _incrementalItemsSource.LoadMoreItemsAsync(count); + + if (_loadingOperation != null) + { + _loadingOperation.Completed = OnLoadingOperationCompleted; + } + } + +#if FEATURE_PAGEDCOLLECTIONVIEW + /// + /// Creates a collection view around the DataGrid's source. ICollectionViewFactory is + /// used if the source implements it. Otherwise a PagedCollectionView is returned. + /// + /// Enumerable source for which to create a view + /// ICollectionView view over the provided source +#else + /// + /// Creates a collection view around the DataGrid's source. ICollectionViewFactory is + /// used if the source implements it. + /// + /// Enumerable source for which to create a view + /// ICollectionView view over the provided source +#endif + internal static ICollectionView CreateView(IEnumerable source) + { + Debug.Assert(source != null, "source unexpectedly null"); + Debug.Assert(!(source is ICollectionView), "source is an ICollectionView"); + + ICollectionView collectionView = null; + + ICollectionViewFactory collectionViewFactory = source as ICollectionViewFactory; + if (collectionViewFactory != null) + { + // If the source is a collection view factory, give it a chance to produce a custom collection view. + collectionView = collectionViewFactory.CreateView(); + + // Intentionally not catching potential exception thrown by ICollectionViewFactory.CreateView(). + } + +#if FEATURE_PAGEDCOLLECTIONVIEW + if (collectionView == null) + { + collectionView = new PagedCollectionView(source); + } +#endif + + if (collectionView == null) + { + IList sourceAsList = source as IList; + if (sourceAsList != null) + { + collectionView = new ListCollectionView(sourceAsList); + } + else + { + collectionView = new EnumerableCollectionView(source); + } + } + + return collectionView; + } + + internal static bool DataTypeIsPrimitive(Type dataType) + { + if (dataType != null) + { + Type type = TypeHelper.GetNonNullableType(dataType); // no-opt if dataType isn't nullable + return + type.GetTypeInfo().IsPrimitive || + type == typeof(string) || + type == typeof(decimal) || + type == typeof(DateTime); + } + else + { + return false; + } + } + + internal void ClearDataProperties() + { + _dataProperties = null; + } + + internal void MoveCurrentTo(object item, int backupSlot, int columnIndex, DataGridSelectionAction action, bool scrollIntoView) + { + if (this.CollectionView != null) + { + _expectingCurrentChanged = true; + _columnForCurrentChanged = columnIndex; + _itemToSelectOnCurrentChanged = item; + _selectionActionForCurrentChanged = action; + _scrollForCurrentChanged = scrollIntoView; + _backupSlotForCurrentChanged = backupSlot; + + var itemIsCollectionViewGroup = item is ICollectionViewGroup; + this.CollectionView.MoveCurrentTo((itemIsCollectionViewGroup || this.IndexOf(item) == this.NewItemPlaceholderIndex) ? null : item); + + _expectingCurrentChanged = false; + } + } + + internal void UnWireEvents(IEnumerable value) + { + INotifyCollectionChanged notifyingDataSource1 = value as INotifyCollectionChanged; + if (notifyingDataSource1 != null && _weakCollectionChangedListener != null) + { + _weakCollectionChangedListener.Detach(); + _weakCollectionChangedListener = null; + } + + IObservableVector notifyingDataSource2 = value as IObservableVector; + if (notifyingDataSource2 != null && _weakVectorChangedListener != null) + { + _weakVectorChangedListener.Detach(); + _weakVectorChangedListener = null; + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + if (this.SortDescriptions != null && _weakSortDescriptionsCollectionChangedListener != null) + { + _weakSortDescriptionsCollectionChangedListener.Detach(); + _weakSortDescriptionsCollectionChangedListener = null; + } +#endif + + if (this.CollectionView != null) + { + if (_weakCurrentChangedListener != null) + { + _weakCurrentChangedListener.Detach(); + _weakCurrentChangedListener = null; + } + + if (_weakCurrentChangingListener != null) + { + _weakCurrentChangingListener.Detach(); + _weakCurrentChangingListener = null; + } + } + + this.EventsWired = false; + } + + internal void WireEvents(IEnumerable value) + { + INotifyCollectionChanged notifyingDataSource1 = value as INotifyCollectionChanged; + if (notifyingDataSource1 != null) + { + _weakCollectionChangedListener = new WeakEventListener(this); + _weakCollectionChangedListener.OnEventAction = (instance, source, eventArgs) => instance.NotifyingDataSource_CollectionChanged(source, eventArgs); + _weakCollectionChangedListener.OnDetachAction = (weakEventListener) => notifyingDataSource1.CollectionChanged -= weakEventListener.OnEvent; + notifyingDataSource1.CollectionChanged += _weakCollectionChangedListener.OnEvent; + } + else + { + IObservableVector notifyingDataSource2 = value as IObservableVector; + if (notifyingDataSource2 != null) + { + _weakVectorChangedListener = new WeakEventListener(this); + _weakVectorChangedListener.OnEventAction = (instance, source, eventArgs) => instance.NotifyingDataSource_VectorChanged(source as IObservableVector, eventArgs); + _weakVectorChangedListener.OnDetachAction = (weakEventListener) => notifyingDataSource2.VectorChanged -= _weakVectorChangedListener.OnEvent; + notifyingDataSource2.VectorChanged += _weakVectorChangedListener.OnEvent; + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + if (this.SortDescriptions != null) + { + INotifyCollectionChanged sortDescriptionsINCC = (INotifyCollectionChanged)this.SortDescriptions; + _weakSortDescriptionsCollectionChangedListener = new WeakEventListener(this); + _weakSortDescriptionsCollectionChangedListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionView_SortDescriptions_CollectionChanged(source, eventArgs); + _weakSortDescriptionsCollectionChangedListener.OnDetachAction = (weakEventListener) => sortDescriptionsINCC.CollectionChanged -= weakEventListener.OnEvent; + sortDescriptionsINCC.CollectionChanged += _weakSortDescriptionsCollectionChangedListener.OnEvent; + } +#endif + + if (this.CollectionView != null) + { + // A local variable must be used in the lambda expression or the CollectionView will leak + ICollectionView collectionView = this.CollectionView; + + _weakCurrentChangedListener = new WeakEventListener(this); + _weakCurrentChangedListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionView_CurrentChanged(source, null); + _weakCurrentChangedListener.OnDetachAction = (weakEventListener) => collectionView.CurrentChanged -= weakEventListener.OnEvent; + this.CollectionView.CurrentChanged += _weakCurrentChangedListener.OnEvent; + + _weakCurrentChangingListener = new WeakEventListener(this); + _weakCurrentChangingListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionView_CurrentChanging(source, eventArgs); + _weakCurrentChangingListener.OnDetachAction = (weakEventListener) => collectionView.CurrentChanging -= weakEventListener.OnEvent; + this.CollectionView.CurrentChanging += _weakCurrentChangingListener.OnEvent; + } + + this.EventsWired = true; + } + + private void CollectionView_CurrentChanged(object sender, object e) + { + if (_expectingCurrentChanged) + { + // Committing Edit could cause our item to move to a group that no longer exists. In + // this case, we need to update the item. + ICollectionViewGroup collectionViewGroup = _itemToSelectOnCurrentChanged as ICollectionViewGroup; + if (collectionViewGroup != null) + { + DataGridRowGroupInfo groupInfo = _owner.RowGroupInfoFromCollectionViewGroup(collectionViewGroup); + if (groupInfo == null) + { + // Move to the next slot if the target slot isn't visible + if (!_owner.IsSlotVisible(_backupSlotForCurrentChanged)) + { + _backupSlotForCurrentChanged = _owner.GetNextVisibleSlot(_backupSlotForCurrentChanged); + } + + // Move to the next best slot if we've moved past all the slots. This could happen if multiple + // groups were removed. + if (_backupSlotForCurrentChanged >= _owner.SlotCount) + { + _backupSlotForCurrentChanged = _owner.GetPreviousVisibleSlot(_owner.SlotCount); + } + + // Update the itemToSelect + int newCurrentPosition = -1; + _itemToSelectOnCurrentChanged = _owner.ItemFromSlot(_backupSlotForCurrentChanged, ref newCurrentPosition); + } + } + + _owner.ProcessSelectionAndCurrency( + _columnForCurrentChanged, + _itemToSelectOnCurrentChanged, + _backupSlotForCurrentChanged, + _selectionActionForCurrentChanged, + _scrollForCurrentChanged); + } + else if (this.CollectionView != null) + { + _owner.UpdateStateOnCurrentChanged(this.CollectionView.CurrentItem, this.CollectionView.CurrentPosition); + } + } + + private void CollectionView_CurrentChanging(object sender, CurrentChangingEventArgs e) + { + if (_owner.NoCurrentCellChangeCount == 0 && + !_expectingCurrentChanged && + !this.EndingEdit && + !_owner.CommitEdit()) + { + // If CommitEdit failed, then the user has most likely input invalid data. + // Cancel the current change if possible, otherwise abort the edit. + if (e.IsCancelable) + { + e.Cancel = true; + } + else + { + _owner.CancelEdit(DataGridEditingUnit.Row, false); + } + } + } + +#if FEATURE_ICOLLECTIONVIEW_SORT + private void CollectionView_SortDescriptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_owner.ColumnsItemsInternal.Count == 0) + { + return; + } + + // Refresh sort description + foreach (DataGridColumn column in _owner.ColumnsItemsInternal) + { + column.HeaderCell.ApplyState(true); + } + } +#endif + + private void NotifyingDataSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (_owner.LoadingOrUnloadingRow) + { + throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows(); + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Debug.Assert(e.NewItems != null, "Unexpected NotifyCollectionChangedAction.Add notification"); + Debug.Assert(this.ShouldAutoGenerateColumns || this.IsGrouping || e.NewItems.Count == 1, "Expected NewItems.Count equals 1."); + NotifyingDataSource_Add(e.NewStartingIndex); + break; + + case NotifyCollectionChangedAction.Remove: + IList removedItems = e.OldItems; + if (removedItems == null || e.OldStartingIndex < 0) + { + Debug.Assert(false, "Unexpected NotifyCollectionChangedAction.Remove notification"); + return; + } + + if (!this.IsGrouping) + { + // If we're grouping then we handle this through the CollectionViewGroup notifications. + // Remove is a single item operation. + foreach (object item in removedItems) + { + Debug.Assert(item != null, "Expected non-null item."); + _owner.RemoveRowAt(e.OldStartingIndex, item); + } + } + + break; + + case NotifyCollectionChangedAction.Replace: + throw new NotSupportedException(); + + case NotifyCollectionChangedAction.Reset: + NotifyingDataSource_Reset(); + break; + } + } + + private void NotifyingDataSource_VectorChanged(IObservableVector sender, IVectorChangedEventArgs e) + { + if (_owner.LoadingOrUnloadingRow) + { + throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows(); + } + + int index = (int)e.Index; + + switch (e.CollectionChange) + { + case CollectionChange.ItemChanged: + throw new NotSupportedException(); + + case CollectionChange.ItemInserted: + NotifyingDataSource_Add(index); + break; + + case CollectionChange.ItemRemoved: + if (!this.IsGrouping) + { + // If we're grouping then we handle this through the CollectionViewGroup notifications. + // Remove is a single item operation. + _owner.RemoveRowAt(index, sender[index]); + } + + break; + + case CollectionChange.Reset: + NotifyingDataSource_Reset(); + break; + } + } + + private void NotifyingDataSource_Add(int index) + { + if (this.ShouldAutoGenerateColumns) + { + // The columns are also affected (not just rows) in this case, so reset everything. + _owner.InitializeElements(false /*recycleRows*/); + } + else if (!this.IsGrouping) + { + // If we're grouping then we handle this through the CollectionViewGroup notifications. + // Add is a single item operation. + _owner.InsertRowAt(index); + } + } + + private void NotifyingDataSource_Reset() + { + // Did the data type change during the reset? If not, we can recycle + // the existing rows instead of having to clear them all. We still need to clear our cached + // values for DataType and DataProperties, though, because the collection has been reset. + Type previousDataType = _dataType; + _dataType = null; + if (previousDataType != this.DataType) + { + ClearDataProperties(); + _owner.InitializeElements(false /*recycleRows*/); + } + else + { + _owner.InitializeElements(!this.ShouldAutoGenerateColumns /*recycleRows*/); + } + } + + private void NotifyingIncrementalItemsSource(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(HasMoreItems)) + { + _owner.LoadMoreDataFromIncrementalItemsSource(); + } + } + + private void OnLoadingOperationCompleted(object info, AsyncStatus status) + { + if (status != AsyncStatus.Started) + { + _loadingOperation = null; + } + } + + private void UpdateDataProperties() + { + Type dataType = this.DataType; + + if (this.DataSource != null && dataType != null && !DataTypeIsPrimitive(dataType)) + { + _dataProperties = dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + Debug.Assert(_dataProperties != null, "Expected non-null _dataProperties."); + } + else + { + _dataProperties = null; + } + } + + private void UpdateIncrementalItemsSource() + { + if (_weakIncrementalItemsSourcePropertyChangedListener != null) + { + _weakIncrementalItemsSourcePropertyChangedListener.Detach(); + _weakIncrementalItemsSourcePropertyChangedListener = null; + } + + // Determine if incremental loading should be used + if (_dataSource is ISupportIncrementalLoading incrementalDataSource) + { + _incrementalItemsSource = incrementalDataSource; + } + else if (_owner.ItemsSource is ISupportIncrementalLoading incrementalItemsSource) + { + _incrementalItemsSource = incrementalItemsSource; + } + else + { + _incrementalItemsSource = default(ISupportIncrementalLoading); + } + + if (_incrementalItemsSource != null && _incrementalItemsSource is INotifyPropertyChanged inpc) + { + _weakIncrementalItemsSourcePropertyChangedListener = new WeakEventListener(this); + _weakIncrementalItemsSourcePropertyChangedListener.OnEventAction = (instance, source, eventArgs) => instance.NotifyingIncrementalItemsSource(source, eventArgs); + _weakIncrementalItemsSourcePropertyChangedListener.OnDetachAction = (weakEventListener) => inpc.PropertyChanged -= weakEventListener.OnEvent; + inpc.PropertyChanged += _weakIncrementalItemsSourcePropertyChangedListener.OnEvent; + } + + if (_loadingOperation != null) + { + _loadingOperation.Cancel(); + _loadingOperation = null; + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDetailsPresenter.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDetailsPresenter.cs new file mode 100644 index 0000000..ed2f878 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDetailsPresenter.cs @@ -0,0 +1,171 @@ +// 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; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Used within the template of a to specify the location in the control's visual tree + /// where the row details are to be added. + /// + public sealed class DataGridDetailsPresenter : Panel + { + /// + /// Gets or sets the height of the content. + /// + /// + /// The height of the content. + /// + public double ContentHeight + { + get { return (double)GetValue(ContentHeightProperty); } + set { SetValue(ContentHeightProperty, value); } + } + + /// + /// Identifies the ContentHeight dependency property. + /// + public static readonly DependencyProperty ContentHeightProperty = + DependencyProperty.Register( + "ContentHeight", + typeof(double), + typeof(DataGridDetailsPresenter), + new PropertyMetadata(0.0, OnContentHeightPropertyChanged)); + + /// + /// ContentHeightProperty property changed handler. + /// + /// DataGridDetailsPresenter. + /// DependencyPropertyChangedEventArgs. + private static void OnContentHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridDetailsPresenter detailsPresenter = d as DataGridDetailsPresenter; + detailsPresenter.InvalidateMeasure(); + } + + private DataGrid OwningGrid + { + get + { + if (this.OwningRow != null) + { + return this.OwningRow.OwningGrid; + } + + return null; + } + } + + internal DataGridRow OwningRow + { + get; + set; + } + + /// + /// Arranges the content of the . + /// + /// + /// The actual size used by the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + double rowGroupSpacerWidth = this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.Width.Value; + double xClip = this.OwningGrid.AreRowGroupHeadersFrozen ? rowGroupSpacerWidth : 0; + double leftEdge = rowGroupSpacerWidth; + double width; + if (this.OwningGrid.AreRowDetailsFrozen) + { + leftEdge += this.OwningGrid.HorizontalOffset; + width = this.OwningGrid.CellsWidth; + } + else + { + xClip += this.OwningGrid.HorizontalOffset; + width = Math.Max(this.OwningGrid.CellsWidth, this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth); + } + + // Details should not extend through the indented area + width -= rowGroupSpacerWidth; + double height = Math.Max(0, double.IsNaN(this.ContentHeight) ? 0 : this.ContentHeight); + + foreach (UIElement child in this.Children) + { + child.Arrange(new Rect(leftEdge, 0, width, height)); + } + + if (this.OwningGrid.AreRowDetailsFrozen) + { + // Frozen Details should not be clipped, similar to frozen cells + this.Clip = null; + } + else + { + // Clip so Details doesn't obstruct elements to the left (the RowHeader by default) as we scroll to the right + RectangleGeometry rg = new RectangleGeometry(); + rg.Rect = new Rect(xClip, 0, Math.Max(0, width - xClip + rowGroupSpacerWidth), height); + this.Clip = rg; + } + + return finalSize; + } + + /// + /// Measures the children of a to + /// prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (this.OwningGrid == null || this.Children.Count == 0) + { + return new Size(0.0, 0.0); + } + + double desiredWidth = this.OwningGrid.AreRowDetailsFrozen ? + this.OwningGrid.CellsWidth : + Math.Max(this.OwningGrid.CellsWidth, this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth); + + desiredWidth -= this.OwningGrid.ColumnsInternal.RowGroupSpacerColumn.Width.Value; + + foreach (UIElement child in this.Children) + { + child.Measure(new Size(desiredWidth, double.PositiveInfinity)); + } + + double desiredHeight = Math.Max(0, double.IsNaN(this.ContentHeight) ? 0 : this.ContentHeight); + + return new Size(desiredWidth, desiredHeight); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridDetailsPresenterAutomationPeer(this); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDisplayData.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDisplayData.cs new file mode 100644 index 0000000..b4dd1e3 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridDisplayData.cs @@ -0,0 +1,391 @@ +// 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.Generic; +using System.Diagnostics; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridDisplayData + { + private Stack _fullyRecycledRows; // list of Rows that have been fully recycled (Collapsed) + private int _headScrollingElements; // index of the row in _scrollingRows that is the first displayed row + private DataGrid _owner; + private Stack _recyclableRows; // list of Rows which have not been fully recycled (avoids Measure in several cases) + private List _scrollingElements; // circular list of displayed elements + private Stack _fullyRecycledGroupHeaders; // list of GroupHeaders that have been fully recycled (Collapsed) + private Stack _recyclableGroupHeaders; // list of GroupHeaders which have not been fully recycled (avoids Measure in several cases) + + public DataGridDisplayData(DataGrid owner) + { + _owner = owner; + + ResetSlotIndexes(); + this.FirstDisplayedScrollingCol = -1; + this.LastTotallyDisplayedScrollingCol = -1; + + _scrollingElements = new List(); + _recyclableRows = new Stack(); + _fullyRecycledRows = new Stack(); + _recyclableGroupHeaders = new Stack(); + _fullyRecycledGroupHeaders = new Stack(); + } + + public int FirstDisplayedScrollingCol + { + get; + set; + } + + public int FirstScrollingSlot + { + get; + set; + } + + public int LastScrollingSlot + { + get; + set; + } + + public int LastTotallyDisplayedScrollingCol + { + get; + set; + } + + public int NumDisplayedScrollingElements + { + get + { + return _scrollingElements.Count; + } + } + + public int NumTotallyDisplayedScrollingElements + { + get; + set; + } + + internal double PendingVerticalScrollHeight + { + get; + set; + } + + internal void AddRecylableRow(DataGridRow row) + { + Debug.Assert(!_recyclableRows.Contains(row), "Expected row parameter to be non-recyclable."); + + row.DetachFromDataGrid(true); + _recyclableRows.Push(row); + } + + internal void AddRecylableRowGroupHeader(DataGridRowGroupHeader groupHeader) + { + Debug.Assert(!_recyclableGroupHeaders.Contains(groupHeader), "Expected groupHeader parameter to be non-recyclable."); + + groupHeader.PropertyName = null; + groupHeader.PropertyValue = null; + groupHeader.IsRecycled = true; + _recyclableGroupHeaders.Push(groupHeader); + } + + internal void ClearElements(bool recycle) + { + ResetSlotIndexes(); + if (recycle) + { + foreach (UIElement element in _scrollingElements) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + if (row.IsRecyclable) + { + AddRecylableRow(row); + } + else + { + row.Clip = new RectangleGeometry(); + } + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { + AddRecylableRowGroupHeader(groupHeader); + } + } + } + } + else + { + _recyclableRows.Clear(); + _fullyRecycledRows.Clear(); + _recyclableGroupHeaders.Clear(); + _fullyRecycledGroupHeaders.Clear(); + } + + _scrollingElements.Clear(); + } + + internal void CorrectSlotsAfterDeletion(int slot, bool wasCollapsed) + { + if (wasCollapsed) + { + if (slot > this.FirstScrollingSlot) + { + this.LastScrollingSlot--; + } + } + else if (_owner.IsSlotVisible(slot)) + { + UnloadScrollingElement(slot, true /*updateSlotInformation*/, true /*wasDeleted*/); + } + + // This cannot be an else condition because if there are 2 rows left, and you delete the first one + // then these indexes need to be updated as well + if (slot < this.FirstScrollingSlot) + { + this.FirstScrollingSlot--; + this.LastScrollingSlot--; + } + } + + internal void CorrectSlotsAfterInsertion(int slot, UIElement element, bool isCollapsed) + { + if (slot < this.FirstScrollingSlot) + { + // The row was inserted above our viewport, just update our indexes + this.FirstScrollingSlot++; + this.LastScrollingSlot++; + } + else if (isCollapsed && (slot <= this.LastScrollingSlot)) + { + this.LastScrollingSlot++; + } + else if ((_owner.GetPreviousVisibleSlot(slot) <= this.LastScrollingSlot) || (this.LastScrollingSlot == -1)) + { + Debug.Assert(element != null, "Expected non-null element."); + + // The row was inserted in our viewport, add it as a scrolling row + LoadScrollingSlot(slot, element, true /*updateSlotInformation*/); + } + } + + private int GetCircularListIndex(int slot, bool wrap) + { + int index = slot - this.FirstScrollingSlot - _headScrollingElements - _owner.GetCollapsedSlotCount(this.FirstScrollingSlot, slot); + return wrap ? index % _scrollingElements.Count : index; + } + + internal void FullyRecycleElements() + { + // Fully recycle Recycleable rows and transfer them to Recycled rows + while (_recyclableRows.Count > 0) + { + DataGridRow row = _recyclableRows.Pop(); + Debug.Assert(row != null, "Expected non-null row."); + row.Visibility = Visibility.Collapsed; + Debug.Assert(!_fullyRecycledRows.Contains(row), "Expected row not in _fullyRecycledRows."); + _fullyRecycledRows.Push(row); + } + + // Fully recycle recyclable GroupHeaders and transfer them to Recycled GroupHeaders + while (_recyclableGroupHeaders.Count > 0) + { + DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); + Debug.Assert(groupHeader != null, "Expected non-null groupHeader."); + groupHeader.Visibility = Visibility.Collapsed; + Debug.Assert(!_fullyRecycledGroupHeaders.Contains(groupHeader), "Expected groupHeader not in _fullyRecycledGroupHeaders."); + _fullyRecycledGroupHeaders.Push(groupHeader); + } + } + + internal UIElement GetDisplayedElement(int slot) + { + Debug.Assert(slot >= this.FirstScrollingSlot, "Expected slot greater than or equal to FirstScrollingSlot."); + Debug.Assert(slot <= this.LastScrollingSlot, "Expected slot less than or equal to LastScrollingSlot."); + + return _scrollingElements[GetCircularListIndex(slot, true /*wrap*/)]; + } + + internal DataGridRow GetDisplayedRow(int rowIndex) + { + return GetDisplayedElement(_owner.SlotFromRowIndex(rowIndex)) as DataGridRow; + } + + // Returns an enumeration of the displayed scrolling rows in order starting with the FirstDisplayedScrollingRow + // Only DataGridRow instances are returned when onlyRows = true + internal IEnumerable GetScrollingElements(bool onlyRows = false) + { + for (int i = 0; i < _scrollingElements.Count; i++) + { + UIElement element = _scrollingElements[(_headScrollingElements + i) % _scrollingElements.Count]; + if (!onlyRows || element is DataGridRow) + { + // _scrollingRows is a circular list that wraps + yield return element; + } + } + } + + internal DataGridRowGroupHeader GetUsedGroupHeader() + { + if (_recyclableGroupHeaders.Count > 0) + { + return _recyclableGroupHeaders.Pop(); + } + else if (_fullyRecycledGroupHeaders.Count > 0) + { + // For fully recycled rows, we need to set the Visibility back to Visible + DataGridRowGroupHeader groupHeader = _fullyRecycledGroupHeaders.Pop(); + groupHeader.Visibility = Visibility.Visible; + return groupHeader; + } + + return null; + } + + internal DataGridRow GetUsedRow() + { + if (_recyclableRows.Count > 0) + { + return _recyclableRows.Pop(); + } + else if (_fullyRecycledRows.Count > 0) + { + // For fully recycled rows, we need to set the Visibility back to Visible + DataGridRow row = _fullyRecycledRows.Pop(); + row.Visibility = Visibility.Visible; + return row; + } + + return null; + } + + // Tracks the row at index rowIndex as a scrolling row + internal void LoadScrollingSlot(int slot, UIElement element, bool updateSlotInformation) + { + if (_scrollingElements.Count == 0) + { + SetScrollingSlots(slot); + _scrollingElements.Add(element); + } + else + { + // The slot should be adjacent to the other slots being displayed + Debug.Assert(slot >= _owner.GetPreviousVisibleSlot(this.FirstScrollingSlot), "Expected slot greater than or equal to _owner.GetPreviousVisibleSlot(this.FirstScrollingSlot)."); + Debug.Assert(slot <= _owner.GetNextVisibleSlot(this.LastScrollingSlot), "Expected slot smaller than or equal to _owner.GetNextVisibleSlot(this.LastScrollingSlot)."); + if (updateSlotInformation) + { + if (slot < this.FirstScrollingSlot) + { + this.FirstScrollingSlot = slot; + } + else + { + this.LastScrollingSlot = _owner.GetNextVisibleSlot(this.LastScrollingSlot); + } + } + + int insertIndex = GetCircularListIndex(slot, false /*wrap*/); + if (insertIndex > _scrollingElements.Count) + { + // We need to wrap around from the bottom to the top of our circular list; as a result the head of the list moves forward + insertIndex -= _scrollingElements.Count; + _headScrollingElements++; + } + + _scrollingElements.Insert(insertIndex, element); + } + } + + private void ResetSlotIndexes() + { + SetScrollingSlots(-1); + this.NumTotallyDisplayedScrollingElements = 0; + _headScrollingElements = 0; + } + + private void SetScrollingSlots(int newValue) + { + this.FirstScrollingSlot = newValue; + this.LastScrollingSlot = newValue; + } + + // Stops tracking the element at the given slot as a scrolling element + internal void UnloadScrollingElement(int slot, bool updateSlotInformation, bool wasDeleted) + { + Debug.Assert(_owner.IsSlotVisible(slot), "Expected slot is visible."); + + int elementIndex = GetCircularListIndex(slot, false /*wrap*/); + if (elementIndex > _scrollingElements.Count) + { + // We need to wrap around from the top to the bottom of our circular list + elementIndex -= _scrollingElements.Count; + _headScrollingElements--; + } + + _scrollingElements.RemoveAt(elementIndex); + + if (updateSlotInformation) + { + if (slot == this.FirstScrollingSlot && !wasDeleted) + { + this.FirstScrollingSlot = _owner.GetNextVisibleSlot(this.FirstScrollingSlot); + } + else + { + this.LastScrollingSlot = _owner.GetPreviousVisibleSlot(this.LastScrollingSlot); + } + + if (this.LastScrollingSlot < this.FirstScrollingSlot) + { + ResetSlotIndexes(); + } + } + } + +#if DEBUG + internal void PrintDisplay() + { + foreach (UIElement element in this.GetScrollingElements()) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Row: {1} ", row.Slot, row.Index)); + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + Debug.WriteLine(string.Format( + System.Globalization.CultureInfo.InvariantCulture, + "Slot: {0} GroupHeader: {1}", + groupHeader.RowGroupInfo.Slot, + groupHeader.RowGroupInfo.CollectionViewGroup.Name)); +#else + Debug.WriteLine(string.Format( + System.Globalization.CultureInfo.InvariantCulture, + "Slot: {0} GroupHeader: {1}", + groupHeader.RowGroupInfo.Slot, + groupHeader.RowGroupInfo.ToString())); +#endif + } + } + } + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridEnumerations.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridEnumerations.cs new file mode 100644 index 0000000..d0bf018 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridEnumerations.cs @@ -0,0 +1,190 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Defines modes that indicates how DataGrid content is copied to the Clipboard. + /// + public enum DataGridClipboardCopyMode + { + /// + /// Disable the DataGrid's ability to copy selected items as text. + /// + None, + + /// + /// Enable the DataGrid's ability to copy selected items as text, but do not include + /// the column header content as the first line in the text that gets copied to the Clipboard. + /// + ExcludeHeader, + + /// + /// Enable the DataGrid's ability to copy selected items as text, and include + /// the column header content as the first line in the text that gets copied to the Clipboard. + /// + IncludeHeader + } + + /// + /// Used to specify action to take out of edit mode. + /// + public enum DataGridEditAction + { + /// + /// Cancel the changes. + /// + Cancel, + + /// + /// Commit edited value. + /// + Commit + } + + // Determines the location and visibility of the editing row. + internal enum DataGridEditingRowLocation + { + Bottom = 0, // The editing row is collapsed below the displayed rows + Inline = 1, // The editing row is visible and displayed + Top = 2 // The editing row is collapsed above the displayed rows + } + + /// + /// Determines whether the inner cells' vertical/horizontal gridlines are shown or not. + /// + [Flags] + public enum DataGridGridLinesVisibility + { + /// + /// None DataGridGridLinesVisibility + /// + None = 0, + + /// + /// Horizontal DataGridGridLinesVisibility + /// + Horizontal = 1, + + /// + /// Vertical DataGridGridLinesVisibility + /// + Vertical = 2, + + /// + /// All DataGridGridLinesVisibility + /// + All = 3, + } + + /// + /// Determines whether the current cell or row is edited. + /// + public enum DataGridEditingUnit + { + /// + /// Cell DataGridEditingUnit + /// + Cell = 0, + + /// + /// Row DataGridEditingUnit + /// + Row = 1, + } + + /// + /// Determines whether the row/column headers are shown or not. + /// + [Flags] + public enum DataGridHeadersVisibility + { + /// + /// Show Row, Column, and Corner Headers + /// + All = Row | Column, + + /// + /// Show only Column Headers with top-right corner Header + /// + Column = 0x01, + + /// + /// Show only Row Headers with bottom-left corner + /// + Row = 0x02, + + /// + /// Don’t show any Headers + /// + None = 0x00 + } + + /// + /// Determines the visibility of the row details. + /// + public enum DataGridRowDetailsVisibilityMode + { + /// + /// Collapsed DataGridRowDetailsVisibilityMode + /// + Collapsed = 2, // Show no details. Developer is in charge of toggling visibility. + + /// + /// Visible DataGridRowDetailsVisibilityMode + /// + Visible = 1, // Show the details section for all rows. + + /// + /// VisibleWhenSelected DataGridRowDetailsVisibilityMode + /// + VisibleWhenSelected = 0 // Show the details section only for the selected row(s). + } + + /// + /// Determines the type of action to take when selecting items. + /// + internal enum DataGridSelectionAction + { + AddCurrentToSelection, + None, + RemoveCurrentFromSelection, + SelectCurrent, + SelectFromAnchorToCurrent + } + + /// + /// Determines the selection model. + /// + public enum DataGridSelectionMode + { + /// + /// Extended DataGridSelectionMode + /// + Extended = 0, + + /// + /// Single DataGridSelectionMode + /// + Single = 1 + } + + /// + /// Determines the sort direction of a column. + /// + public enum DataGridSortDirection + { + /// + /// Sorts in ascending order. + /// + Ascending = 0, + + /// + /// Sorts in descending order. + /// + Descending = 1 + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridError.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridError.cs new file mode 100644 index 0000000..a2d3868 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridError.cs @@ -0,0 +1,229 @@ +// 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; +using System.Globalization; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal static class DataGridError + { + public static class DataGrid + { + public static InvalidOperationException CannotChangeItemsWhenLoadingRows() + { + return new InvalidOperationException("Items cannot be added, removed or reset while rows are loading or unloading."); + } + + public static InvalidOperationException CannotChangeColumnCollectionWhileAdjustingDisplayIndexes() + { + return new InvalidOperationException("Column collection cannot be changed while adjusting display indexes."); + } + + public static InvalidOperationException ColumnCannotBeCollapsed() + { + return new InvalidOperationException("Column cannot be collapsed."); + } + + public static InvalidOperationException ColumnCannotBeReassignedToDifferentDataGrid() + { + return new InvalidOperationException("Column already belongs to a DataGrid instance and cannot be reassigned."); + } + + public static ArgumentException ColumnNotInThisDataGrid() + { + return new ArgumentException("Provided column does not belong to this DataGrid."); + } + + public static ArgumentException ItemIsNotContainedInTheItemsSource(string paramName) + { + return new ArgumentException("The item is not contained in the ItemsSource.", paramName); + } + + public static InvalidOperationException NoCurrentRow() + { + return new InvalidOperationException("There is no current row. Operation cannot be completed."); + } + + public static InvalidOperationException NoOwningGrid(Type type) + { + return new InvalidOperationException(Format("There is no instance of DataGrid assigned to this {0}. Operation cannot be completed.", type.FullName)); + } + + public static InvalidOperationException UnderlyingPropertyIsReadOnly(string paramName) + { + return new InvalidOperationException(Format("{0} cannot be set because the underlying property is read only.", paramName)); + } + + public static ArgumentException ValueCannotBeSetToInfinity(string paramName) + { + return new ArgumentException(Format("{0} cannot be set to infinity.", paramName)); + } + + public static ArgumentException ValueCannotBeSetToNAN(string paramName) + { + return new ArgumentException(Format("{0} cannot be set to double.NAN.", paramName)); + } + + public static ArgumentNullException ValueCannotBeSetToNull(string paramName, string valueName) + { + return new ArgumentNullException(paramName, Format("{0} cannot be set to a null value.", valueName)); + } + + public static ArgumentException ValueIsNotAnInstanceOf(string paramName, Type type) + { + return new ArgumentException(paramName, Format("The value is not an instance of {0}.", type.FullName)); + } + + public static ArgumentException ValueIsNotAnInstanceOfEitherOr(string paramName, Type type1, Type type2) + { + return new ArgumentException(paramName, Format("The value is not an instance of {0} or {1}.", type1.FullName, type2.FullName)); + } + + public static ArgumentOutOfRangeException ValueMustBeBetween(string paramName, string valueName, object lowValue, bool lowInclusive, object highValue, bool highInclusive) + { + string message; + + if (lowInclusive && highInclusive) + { + message = "{0} must be greater than or equal to {1} and less than or equal to {2}."; + } + else if (lowInclusive && !highInclusive) + { + message = "{0} must be greater than or equal to {1} and less than {2}."; + } + else if (!lowInclusive && highInclusive) + { + message = "{0} must be greater than {1} and less than or equal to {2}."; + } + else + { + message = "{0} must be greater than {1} and less than {2}."; + } + + return new ArgumentOutOfRangeException(paramName, Format(message, valueName, lowValue, highValue)); + } + + public static ArgumentOutOfRangeException ValueMustBeGreaterThanOrEqualTo(string paramName, string valueName, object value) + { + return new ArgumentOutOfRangeException(paramName, Format("{0} must be greater than or equal to {1}.", valueName, value)); + } + + public static ArgumentOutOfRangeException ValueMustBeLessThanOrEqualTo(string paramName, string valueName, object value) + { + return new ArgumentOutOfRangeException(paramName, Format("{0} must be less than or equal to {1}.", valueName, value)); + } + + public static ArgumentOutOfRangeException ValueMustBeLessThan(string paramName, string valueName, object value) + { + return new ArgumentOutOfRangeException(paramName, Format("{0} must be less than {1}.", valueName, value)); + } + } + + public static class DataGridAutomationPeer + { + public static InvalidOperationException OperationCannotBePerformed() + { + return new InvalidOperationException("Cannot perform the operation."); + } + } + + public static class DataGridColumnHeader + { + public static NotSupportedException ContentDoesNotSupportUIElements() + { + return new NotSupportedException("Content does not support UIElement; use ContentTemplate instead."); + } + } + + public static class DataGridLength + { + public static ArgumentException InvalidUnitType(string paramName) + { + return new ArgumentException(Format("{0} is not a valid DataGridLengthUnitType.", paramName), paramName); + } + } + + public static class DataGridLengthConverter + { + public static NotSupportedException CannotConvertFrom(string paramName) + { + return new NotSupportedException(Format("DataGridLengthConverter cannot convert from {0}.", paramName)); + } + + public static NotSupportedException CannotConvertTo(string paramName) + { + return new NotSupportedException(Format("Cannot convert from DataGridLength to {0}.", paramName)); + } + + public static NotSupportedException InvalidDataGridLength(string paramName) + { + return new NotSupportedException(Format("Invalid DataGridLength.", paramName)); + } + } + + public static class DataGridRow + { + public static InvalidOperationException InvalidRowIndexCannotCompleteOperation() + { + return new InvalidOperationException("Invalid row index. Operation cannot be completed."); + } + } + + public static class DataGridSelectedItemsCollection + { + public static InvalidOperationException CannotChangeSelectedItemsCollectionInSingleMode() + { + return new InvalidOperationException("Can only change SelectedItems collection in Extended selection mode. Use SelectedItem property in Single selection mode."); + } + } + + public static class DataGridComboBoxColumn + { + public static ArgumentException UnsetBinding(string header) + { + return new ArgumentException(Format("Binding for column {0} is null. Ensure that the binding path has been set correctly.", header)); + } + + public static ArgumentException UnsetBinding(Type type) + { + return new ArgumentException(Format("Binding for column of type {0} is null. Ensure that the binding path has been set correctly.", type.FullName)); + } + + public static ArgumentException UnknownBindingPath(Binding binding, Type type) + { + return new ArgumentException(Format("Binding path {0} could not be found in type {1}. Ensure that the binding path has been set correctly.", binding.Path.Path, type.FullName)); + } + + public static ArgumentException UnknownDisplayMemberPath(string displayMemberPath, Type type) + { + return new ArgumentException(Format("DisplayMemberPath {0} could not be found in type {1}. Ensure that the value has been set correctly and note that for built-in types DisplayMemberPath should not be used.", displayMemberPath, type.FullName)); + } + + public static ArgumentException UnknownItemsSourcePath(Binding binding) + { + return new ArgumentException(Format("The ItemsSource elements do not contain a property {0}. Ensure that the binding path has been set correctly.", binding.Path.Path)); + } + + public static ArgumentException BindingTypeMismatch(Type bindingType, Type itemSourceType) + { + return new ArgumentException(Format("The DataGridComboBoxColumn ItemSource elements of type \'{0}\' do not match the Binding type \'{1}\'. Ensure that the paths have been set correctly and specify a DisplayMemberPath for non built-in types.", itemSourceType.FullName, bindingType.FullName)); + } + } + + public static class DataGridTemplateColumn + { + public static TypeInitializationException MissingTemplateForType(Type type) + { + return new TypeInitializationException(Format("Missing template. Cannot initialize {0}.", type.FullName), null); + } + } + + private static string Format(string formatString, params object[] args) + { + return string.Format(CultureInfo.CurrentCulture, formatString, args); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFillerColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFillerColumn.cs new file mode 100644 index 0000000..a3e8471 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFillerColumn.cs @@ -0,0 +1,74 @@ +// 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.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + internal class DataGridFillerColumn : DataGridColumn + { + public DataGridFillerColumn(DataGrid owningGrid) + { + this.IsReadOnly = true; + this.OwningGrid = owningGrid; + this.MinWidth = 0; + this.MaxWidth = int.MaxValue; + } + + internal double FillerWidth + { + get; + set; + } + + // True if there is room for the filler column; otherwise, false + internal bool IsActive + { + get + { + return this.FillerWidth > 0; + } + } + + // True if the FillerColumn's header cell is contained in the visual tree + internal bool IsRepresented + { + get; + set; + } + + internal override DataGridColumnHeader CreateHeader() + { + DataGridColumnHeader headerCell = base.CreateHeader(); + if (headerCell != null) + { + Windows.UI.Xaml.Automation.AutomationProperties.SetAccessibilityView( + headerCell, + Windows.UI.Xaml.Automation.Peers.AccessibilityView.Raw); + headerCell.IsEnabled = false; + } + + return headerCell; + } + + protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) + { + return null; + } + + protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) + { + return null; + } + + protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + Debug.Assert(false, "Unexpected call to DataGridFillerColumn.PrepareCellForEdit."); + + return null; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFrozenGrid.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFrozenGrid.cs new file mode 100644 index 0000000..b8bf85b --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridFrozenGrid.cs @@ -0,0 +1,58 @@ +// 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; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Represents a non-scrollable grid that contains row headers. + /// + public class DataGridFrozenGrid : Grid + { + /// + /// A dependency property that indicates whether the grid is frozen. + /// + public static readonly DependencyProperty IsFrozenProperty = DependencyProperty.RegisterAttached( + "IsFrozen", + typeof(bool), + typeof(DataGridFrozenGrid), + null); + + /// + /// Gets a value that indicates whether the grid is frozen. + /// + /// + /// The object to get the IsFrozen value from. + /// + /// true if the grid is frozen; otherwise, false. The default is true. + public static bool GetIsFrozen(DependencyObject element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return (bool)element.GetValue(IsFrozenProperty); + } + + /// + /// Sets a value that indicates whether the grid is frozen. + /// + /// The object to set the IsFrozen value on. + /// true if is frozen; otherwise, false. + /// is null. + public static void SetIsFrozen(DependencyObject element, bool value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(IsFrozenProperty, value); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridInteractionInfo.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridInteractionInfo.cs new file mode 100644 index 0000000..aea502e --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridInteractionInfo.cs @@ -0,0 +1,23 @@ +// 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 Windows.UI.Xaml.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridInteractionInfo + { + internal uint CapturedPointerId + { + get; + set; + } + + internal bool IsPointerOver + { + get; + set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridLength.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridLength.cs new file mode 100644 index 0000000..76bf071 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridLength.cs @@ -0,0 +1,477 @@ +// 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; +using System.Globalization; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.Utilities; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// DataGridLengthUnitType + /// + /// + /// These aren't flags. + /// + public enum DataGridLengthUnitType + { + /// + /// Auto DataGridLengthUnitType + /// + Auto = 0, + + /// + /// Pixel DataGridLengthUnitType + /// + Pixel = 1, + + /// + /// SizeToCells DataGridLengthUnitType + /// + SizeToCells = 2, + + /// + /// SizeToHeader DataGridLengthUnitType + /// + SizeToHeader = 3, + + /// + /// Star DataGridLengthUnitType + /// + Star = 4 + } + + /// + /// Represents the lengths of elements within the control. + /// + [Windows.Foundation.Metadata.CreateFromString(MethodName = "Microsoft.Toolkit.Uwp.UI.Controls.DataGridLength.ConvertFromString")] + public struct DataGridLength : IEquatable + { + // static instances of value invariant DataGridLengths + private static readonly DataGridLength _auto = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.Auto); + private static readonly DataGridLength _sizeToCells = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.SizeToCells); + private static readonly DataGridLength _sizeToHeader = new DataGridLength(DATAGRIDLENGTH_DefaultValue, DataGridLengthUnitType.SizeToHeader); + + private static string _starSuffix = "*"; + private static string[] _valueInvariantUnitStrings = { "auto", "sizetocells", "sizetoheader" }; + private static DataGridLength[] _valueInvariantDataGridLengths = { DataGridLength.Auto, DataGridLength.SizeToCells, DataGridLength.SizeToHeader }; + + private double _desiredValue; // desired value storage + private double _displayValue; // display value storage + private double _unitValue; // unit value storage + private DataGridLengthUnitType _unitType; // unit type storage + + internal const double DATAGRIDLENGTH_DefaultValue = 1.0; + + /// + /// Initializes a new instance of the struct based on a numerical value. + /// + /// numerical length + public DataGridLength(double value) + : this(value, DataGridLengthUnitType.Pixel) + { + } + + /// + /// Initializes a new instance of the struct based on a numerical value and a type. + /// + /// The value to hold. + /// The unit of value. + /// + /// value is ignored unless type is + /// DataGridLengthUnitType.Pixel or + /// DataGridLengthUnitType.Star + /// + /// + /// If value parameter is double.NaN + /// or value parameter is double.NegativeInfinity + /// or value parameter is double.PositiveInfinity. + /// + public DataGridLength(double value, DataGridLengthUnitType type) + : this(value, type, type == DataGridLengthUnitType.Pixel ? value : double.NaN, type == DataGridLengthUnitType.Pixel ? value : double.NaN) + { + } + + /// + /// Initializes a new instance of the struct based on a numerical value and a unit. + /// + /// The value to hold. + /// The unit of value. + /// The desired value. + /// The display value. + /// + /// value is ignored unless type is + /// DataGridLengthUnitType.Pixel or + /// DataGridLengthUnitType.Star + /// + /// + /// If value parameter is double.NaN + /// or value parameter is double.NegativeInfinity + /// or value parameter is double.PositiveInfinity. + /// + public DataGridLength(double value, DataGridLengthUnitType type, double desiredValue, double displayValue) + { + if (double.IsNaN(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN("value"); + } + + if (double.IsInfinity(value)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("value"); + } + + if (double.IsInfinity(desiredValue)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("desiredValue"); + } + + if (double.IsInfinity(displayValue)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("displayValue"); + } + + if (value < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "value", 0); + } + + if (desiredValue < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("desiredValue", "desiredValue", 0); + } + + if (displayValue < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("displayValue", "displayValue", 0); + } + + if (type != DataGridLengthUnitType.Auto && + type != DataGridLengthUnitType.SizeToCells && + type != DataGridLengthUnitType.SizeToHeader && + type != DataGridLengthUnitType.Star && + type != DataGridLengthUnitType.Pixel) + { + throw DataGridError.DataGridLength.InvalidUnitType("type"); + } + + _desiredValue = desiredValue; + _displayValue = displayValue; + _unitValue = (type == DataGridLengthUnitType.Auto) ? DATAGRIDLENGTH_DefaultValue : value; + _unitType = type; + } + + /// + /// Gets a structure that represents the standard automatic sizing mode. + /// + /// + /// A structure that represents the standard automatic sizing mode. + /// + public static DataGridLength Auto + { + get + { + return _auto; + } + } + + /// + /// Gets a structure that represents the cell-based automatic sizing mode. + /// + /// + /// A structure that represents the cell-based automatic sizing mode. + /// + public static DataGridLength SizeToCells + { + get + { + return _sizeToCells; + } + } + + /// + /// Gets a structure that represents the header-based automatic sizing mode. + /// + /// + /// A structure that represents the header-based automatic sizing mode. + /// + public static DataGridLength SizeToHeader + { + get + { + return _sizeToHeader; + } + } + + /// + /// Gets the desired value of this instance. + /// + public double DesiredValue + { + get + { + return _desiredValue; + } + } + + /// + /// Gets the display value of this instance. + /// + public double DisplayValue + { + get + { + return _displayValue; + } + } + + /// + /// Gets a value indicating whether this DataGridLength instance holds an absolute (pixel) value. + /// + public bool IsAbsolute + { + get + { + return _unitType == DataGridLengthUnitType.Pixel; + } + } + + /// + /// Gets a value indicating whether this DataGridLength instance is automatic (not specified). + /// + public bool IsAuto + { + get + { + return _unitType == DataGridLengthUnitType.Auto; + } + } + + /// + /// Gets a value indicating whether this DataGridLength instance is to size to the cells of a column or row. + /// + public bool IsSizeToCells + { + get + { + return _unitType == DataGridLengthUnitType.SizeToCells; + } + } + + /// + /// Gets a value indicating whether this DataGridLength instance is to size to the header of a column or row. + /// + public bool IsSizeToHeader + { + get + { + return _unitType == DataGridLengthUnitType.SizeToHeader; + } + } + + /// + /// Gets a value indicating whether this DataGridLength instance holds a weighted proportion of available space. + /// + public bool IsStar + { + get + { + return _unitType == DataGridLengthUnitType.Star; + } + } + + /// + /// Gets the that represents the current sizing mode. + /// + public DataGridLengthUnitType UnitType + { + get + { + return _unitType; + } + } + + /// + /// Gets the absolute value of the in pixels. + /// + /// + /// The absolute value of the in pixels. + /// + public double Value + { + get + { + return _unitValue; + } + } + + /// + /// Converts a string into a instance. + /// + /// string to convert. + /// The result of the conversion. + public static DataGridLength ConvertFromString(string value) + { + return ConvertFrom(null, value); + } + + /// + /// Converts an object into a instance. + /// + /// optional culture to use for conversion. + /// object to convert. + /// The result of the conversion. + public static DataGridLength ConvertFrom(CultureInfo culture, object value) + { + if (value == null) + { + throw DataGridError.DataGridLengthConverter.CannotConvertFrom("(null)"); + } + + string stringValue = value as string; + if (stringValue != null) + { + stringValue = stringValue.Trim(); + + if (stringValue.EndsWith(_starSuffix, StringComparison.Ordinal)) + { + string stringValueWithoutSuffix = stringValue.Substring(0, stringValue.Length - _starSuffix.Length); + + double starWeight; + if (string.IsNullOrEmpty(stringValueWithoutSuffix)) + { + starWeight = 1; + } + else + { + starWeight = Convert.ToDouble(stringValueWithoutSuffix, culture ?? CultureInfo.CurrentCulture); + } + + return new DataGridLength(starWeight, DataGridLengthUnitType.Star); + } + + for (int index = 0; index < _valueInvariantUnitStrings.Length; index++) + { + if (stringValue.Equals(_valueInvariantUnitStrings[index], StringComparison.OrdinalIgnoreCase)) + { + return _valueInvariantDataGridLengths[index]; + } + } + } + + // Conversion from numeric type + double doubleValue = Convert.ToDouble(value, culture ?? CultureInfo.CurrentCulture); + if (double.IsNaN(doubleValue)) + { + return DataGridLength.Auto; + } + else + { + return new DataGridLength(doubleValue); + } + } + + /// + /// Converts a instance into a string. + /// + /// optional culture to use for conversion. + /// value to convert. + /// The result of the conversion. + public static string ConvertToString(CultureInfo culture, DataGridLength value) + { + // Convert dataGridLength to a string + switch (value.UnitType) + { + // for Auto print out "Auto". value is always "1.0" + case DataGridLengthUnitType.Auto: + return "Auto"; + + case DataGridLengthUnitType.SizeToHeader: + return "SizeToHeader"; + + case DataGridLengthUnitType.SizeToCells: + return "SizeToCells"; + + // Star has one special case when value is "1.0". + // in this case drop value part and print only "Star" + case DataGridLengthUnitType.Star: + return + DoubleUtil.AreClose(1.0, value.Value) + ? _starSuffix + : Convert.ToString(value.Value, culture ?? CultureInfo.CurrentCulture) + _starSuffix; + + default: + return Convert.ToString(value.Value, culture ?? CultureInfo.CurrentCulture); + } + } + + /// + /// Overloaded operator, compares 2 DataGridLength's. + /// + /// first DataGridLength to compare. + /// second DataGridLength to compare. + /// true if specified DataGridLength have same value, + /// unit type, desired value, and display value. + public static bool operator ==(DataGridLength gl1, DataGridLength gl2) + { + return gl1.UnitType == gl2.UnitType && + gl1.Value == gl2.Value && + gl1.DesiredValue == gl2.DesiredValue && + gl1.DisplayValue == gl2.DisplayValue; + } + + /// + /// Overloaded operator, compares 2 DataGridLength's. + /// + /// first DataGridLength to compare. + /// second DataGridLength to compare. + /// true if specified DataGridLength have either different value, + /// unit type, desired value, or display value. + public static bool operator !=(DataGridLength gl1, DataGridLength gl2) + { + return gl1.UnitType != gl2.UnitType || + gl1.Value != gl2.Value || + gl1.DesiredValue != gl2.DesiredValue || + gl1.DisplayValue != gl2.DisplayValue; + } + + /// + /// Compares this instance of DataGridLength with another instance. + /// + /// DataGridLength length instance to compare. + /// true if this DataGridLength instance has the same value + /// and unit type as gridLength. + public bool Equals(DataGridLength other) + { + return this == other; + } + + /// + /// Compares this instance of DataGridLength with another object. + /// + /// Reference to an object for comparison. + /// true if this DataGridLength instance has the same value + /// and unit type as oCompare. + public override bool Equals(object obj) + { + DataGridLength? dataGridLength = obj as DataGridLength?; + if (dataGridLength.HasValue) + { + return this == dataGridLength; + } + + return false; + } + + /// + /// Returns a unique hash code for this DataGridLength + /// + /// hash code + public override int GetHashCode() + { + return (int)_unitValue + (int)_unitType + (int)_desiredValue + (int)_displayValue; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridPreparingCellForEditEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridPreparingCellForEditEventArgs.cs new file mode 100644 index 0000000..f5ef746 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridPreparingCellForEditEventArgs.cs @@ -0,0 +1,70 @@ +// 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; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for the event. + /// + public class DataGridPreparingCellForEditEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The column that contains the cell to be edited. + /// The row that contains the cell to be edited. + /// Information about the user gesture that caused the cell to enter edit mode. + /// The element that the column displays for a cell in editing mode. + public DataGridPreparingCellForEditEventArgs( + DataGridColumn column, + DataGridRow row, + RoutedEventArgs editingEventArgs, + FrameworkElement editingElement) + { + this.Column = column; + this.Row = row; + this.EditingEventArgs = editingEventArgs; + this.EditingElement = editingElement; + } + + /// + /// Gets the column that contains the cell to be edited. + /// + public DataGridColumn Column + { + get; + private set; + } + + /// + /// Gets the element that the column displays for a cell in editing mode. + /// + public FrameworkElement EditingElement + { + get; + private set; + } + + /// + /// Gets information about the user gesture that caused the cell to enter edit mode. + /// + public RoutedEventArgs EditingEventArgs + { + get; + private set; + } + + /// + /// Gets the row that contains the cell to be edited. + /// + public DataGridRow Row + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRow.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRow.cs new file mode 100644 index 0000000..aae0554 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRow.cs @@ -0,0 +1,1532 @@ +// 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; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Devices.Input; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Shapes; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a row. + /// + [TemplatePart(Name = DATAGRIDROW_elementBottomGridLine, Type = typeof(Rectangle))] + [TemplatePart(Name = DATAGRIDROW_elementCells, Type = typeof(DataGridCellsPresenter))] + [TemplatePart(Name = DATAGRIDROW_elementDetails, Type = typeof(DataGridDetailsPresenter))] + [TemplatePart(Name = DATAGRIDROW_elementRoot, Type = typeof(Panel))] + [TemplatePart(Name = DATAGRIDROW_elementRowHeader, Type = typeof(DataGridRowHeader))] + + [TemplateVisualState(Name = DATAGRIDROW_stateNormal, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_stateAlternate, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_stateNormalEditing, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_stateNormalEditingFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = DATAGRIDROW_stateSelected, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_stateSelectedFocused, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_statePointerOver, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_statePointerOverEditing, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_statePointerOverEditingFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = DATAGRIDROW_statePointerOverSelected, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROW_statePointerOverSelectedFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = VisualStates.StateInvalid, GroupName = VisualStates.GroupValidation)] + [TemplateVisualState(Name = VisualStates.StateValid, GroupName = VisualStates.GroupValidation)] + [StyleTypedProperty(Property = "HeaderStyle", StyleTargetType = typeof(DataGridRowHeader))] + public partial class DataGridRow : Control + { + private const byte DATAGRIDROW_defaultMinHeight = 0; + internal const int DATAGRIDROW_maximumHeight = 65536; + internal const double DATAGRIDROW_minimumHeight = 0; + + private const string DATAGRIDROW_elementBottomGridLine = "BottomGridLine"; + private const string DATAGRIDROW_elementCells = "CellsPresenter"; + private const string DATAGRIDROW_elementDetails = "DetailsPresenter"; + internal const string DATAGRIDROW_elementRoot = "RowRoot"; + internal const string DATAGRIDROW_elementRowHeader = "RowHeader"; + + private const string DATAGRIDROW_stateAlternate = "NormalAlternatingRow"; + private const string DATAGRIDROW_statePointerOver = "PointerOver"; + private const string DATAGRIDROW_statePointerOverEditing = "PointerOverUnfocusedEditing"; + private const string DATAGRIDROW_statePointerOverEditingFocused = "PointerOverEditing"; + private const string DATAGRIDROW_statePointerOverSelected = "PointerOverUnfocusedSelected"; + private const string DATAGRIDROW_statePointerOverSelectedFocused = "PointerOverSelected"; + private const string DATAGRIDROW_stateNormal = "Normal"; + private const string DATAGRIDROW_stateNormalEditing = "UnfocusedEditing"; + private const string DATAGRIDROW_stateNormalEditingFocused = "NormalEditing"; + private const string DATAGRIDROW_stateSelected = "UnfocusedSelected"; + private const string DATAGRIDROW_stateSelectedFocused = "NormalSelected"; + + private const byte DATAGRIDROW_statePointerOverCode = 0; + private const byte DATAGRIDROW_statePointerOverEditingCode = 1; + private const byte DATAGRIDROW_statePointerOverEditingFocusedCode = 2; + private const byte DATAGRIDROW_statePointerOverSelectedCode = 3; + private const byte DATAGRIDROW_statePointerOverSelectedFocusedCode = 4; + private const byte DATAGRIDROW_stateNormalCode = 5; + private const byte DATAGRIDROW_stateNormalEditingCode = 6; + private const byte DATAGRIDROW_stateNormalEditingFocusedCode = 7; + private const byte DATAGRIDROW_stateSelectedCode = 8; + private const byte DATAGRIDROW_stateSelectedFocusedCode = 9; + private const byte DATAGRIDROW_stateNullCode = 255; + + // Static arrays to handle state transitions: + private static byte[] _idealStateMapping = new byte[] + { + DATAGRIDROW_stateNormalCode, + DATAGRIDROW_stateNormalCode, + DATAGRIDROW_statePointerOverCode, + DATAGRIDROW_statePointerOverCode, + DATAGRIDROW_stateNullCode, + DATAGRIDROW_stateNullCode, + DATAGRIDROW_stateNullCode, + DATAGRIDROW_stateNullCode, + DATAGRIDROW_stateSelectedCode, + DATAGRIDROW_stateSelectedFocusedCode, + DATAGRIDROW_statePointerOverSelectedCode, + DATAGRIDROW_statePointerOverSelectedFocusedCode, + DATAGRIDROW_stateNormalEditingCode, + DATAGRIDROW_stateNormalEditingFocusedCode, + DATAGRIDROW_statePointerOverEditingCode, + DATAGRIDROW_statePointerOverEditingFocusedCode + }; + + private static byte[] _fallbackStateMapping = new byte[] + { + DATAGRIDROW_stateNormalCode, // DATAGRIDROW_statePointerOverCode's fallback + DATAGRIDROW_statePointerOverEditingFocusedCode, // DATAGRIDROW_statePointerOverEditingCode's fallback + DATAGRIDROW_stateNormalEditingFocusedCode, // DATAGRIDROW_statePointerOverEditingFocusedCode's fallback + DATAGRIDROW_statePointerOverSelectedFocusedCode, // DATAGRIDROW_statePointerOverSelectedCode's fallback + DATAGRIDROW_stateSelectedFocusedCode, // DATAGRIDROW_statePointerOverSelectedFocusedCode's fallback + DATAGRIDROW_stateNullCode, // DATAGRIDROW_stateNormalCode's fallback + DATAGRIDROW_stateNormalEditingFocusedCode, // DATAGRIDROW_stateNormalEditingCode's fallback + DATAGRIDROW_stateSelectedFocusedCode, // DATAGRIDROW_stateNormalEditingFocusedCode's fallback + DATAGRIDROW_stateSelectedFocusedCode, // DATAGRIDROW_stateSelectedCode's fallback + DATAGRIDROW_stateNormalCode // DATAGRIDROW_stateSelectedFocusedCode's fallback + }; + + private static string[] _stateNames = new string[] + { + DATAGRIDROW_statePointerOver, + DATAGRIDROW_statePointerOverEditing, + DATAGRIDROW_statePointerOverEditingFocused, + DATAGRIDROW_statePointerOverSelected, + DATAGRIDROW_statePointerOverSelectedFocused, + DATAGRIDROW_stateNormal, + DATAGRIDROW_stateNormalEditing, + DATAGRIDROW_stateNormalEditingFocused, + DATAGRIDROW_stateSelected, + DATAGRIDROW_stateSelectedFocused + }; + + // Locally cache whether or not details are visible so we don't run redundant storyboards + // The Details Template that is actually applied to the Row + private DataTemplate _appliedDetailsTemplate; + private Visibility? _appliedDetailsVisibility; + private Rectangle _bottomGridLine; + private DataGridCellsPresenter _cellsElement; + + // In the case where Details scales vertically when it's arranged at a different width, we + // get the wrong height measurement so we need to check it again after arrange + private bool _checkDetailsContentHeight; + + private Brush _computedForeground; + + // Optimal height of the details based on the Element created by the DataTemplate + private double _detailsDesiredHeight; + private bool _detailsLoaded; + private bool _detailsVisibilityNotificationPending; + private FrameworkElement _detailsContent; + private DataGridDetailsPresenter _detailsElement; + private DataGridCell _fillerCell; + private DataGridRowHeader _headerElement; + private double _lastHorizontalOffset; + + /// + /// Initializes a new instance of the class. + /// + public DataGridRow() + { + this.MinHeight = DATAGRIDROW_defaultMinHeight; + this.IsTapEnabled = true; + this.Index = -1; + this.IsValid = true; + this.Slot = -1; + _detailsDesiredHeight = double.NaN; + _detailsLoaded = false; + _appliedDetailsVisibility = Visibility.Collapsed; + _computedForeground = this.Foreground; + this.Cells = new DataGridCellCollection(this); + this.Cells.CellAdded += new EventHandler(DataGridCellCollection_CellAdded); + this.Cells.CellRemoved += new EventHandler(DataGridCellCollection_CellRemoved); + + this.AddHandler(UIElement.TappedEvent, new TappedEventHandler(DataGridRow_Tapped), true /*handledEventsToo*/); + + this.PointerCanceled += new PointerEventHandler(DataGridRow_PointerCanceled); + this.PointerCaptureLost += new PointerEventHandler(DataGridRow_PointerCaptureLost); + this.PointerPressed += new PointerEventHandler(DataGridRow_PointerPressed); + this.PointerReleased += new PointerEventHandler(DataGridRow_PointerReleased); + this.PointerEntered += new PointerEventHandler(DataGridRow_PointerEntered); + this.PointerExited += new PointerEventHandler(DataGridRow_PointerExited); + this.PointerMoved += new PointerEventHandler(DataGridRow_PointerMoved); + + RegisterPropertyChangedCallback(ForegroundProperty, OnDependencyPropertyChanged); + + DefaultStyleKey = typeof(DataGridRow); + } + + /// + /// Gets or sets the template that is used to display the details section of the row. + /// + public DataTemplate DetailsTemplate + { + get { return GetValue(DetailsTemplateProperty) as DataTemplate; } + set { SetValue(DetailsTemplateProperty, value); } + } + + /// + /// Identifies the DetailsTemplate dependency property. + /// + public static readonly DependencyProperty DetailsTemplateProperty = + DependencyProperty.Register( + "DetailsTemplate", + typeof(DataTemplate), + typeof(DataGridRow), + new PropertyMetadata(null, OnDetailsTemplatePropertyChanged)); + + /// + /// DetailsTemplateProperty property changed handler. + /// + /// DataGridRow that changed its DetailsTemplate. + /// DependencyPropertyChangedEventArgs. + private static void OnDetailsTemplatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRow source = d as DataGridRow; + Debug.Assert(source != null, "The source is not an instance of DataGridRow!"); + + Debug.Assert( + (e.NewValue == null) || + typeof(DataTemplate).IsInstanceOfType(e.NewValue), + "The e.NewValue is not an instance of DataTemplate."); + DataTemplate newValue = (DataTemplate)e.NewValue; + DataTemplate oldValue = (DataTemplate)e.OldValue; + + if (!source.IsHandlerSuspended(e.Property) && source.OwningGrid != null) + { + Func actualDetailsTemplate = template => (template != null ? template : source.OwningGrid.RowDetailsTemplate); + + // We don't always want to apply the new Template because they might have set the same one + // we inherited from the DataGrid + if (actualDetailsTemplate(newValue) != actualDetailsTemplate(oldValue)) + { + source.ApplyDetailsTemplate(false /*initializeDetailsPreferredHeight*/); + } + } + } + + /// + /// Gets or sets a value that indicates when the details section of the row is displayed. + /// + public Visibility DetailsVisibility + { + get { return (Visibility)GetValue(DetailsVisibilityProperty); } + set { SetValue(DetailsVisibilityProperty, value); } + } + + /// + /// Identifies the DetailsTemplate dependency property. + /// + public static readonly DependencyProperty DetailsVisibilityProperty = + DependencyProperty.Register( + "DetailsVisibility", + typeof(Visibility), + typeof(DataGridRow), + new PropertyMetadata(Visibility.Collapsed, OnDetailsVisibilityPropertyChanged)); + + /// + /// DetailsVisibilityProperty property changed handler. + /// + /// DataGridRow that changed its DetailsTemplate. + /// DependencyPropertyChangedEventArgs. + private static void OnDetailsVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRow row = d as DataGridRow; + + if (!row.IsHandlerSuspended(e.Property)) + { + if (row.OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(row.GetType()); + } + + if (row.Index == -1) + { + throw DataGridError.DataGridRow.InvalidRowIndexCannotCompleteOperation(); + } + + Visibility newValue = (Visibility)e.NewValue; + row.OwningGrid.OnRowDetailsVisibilityPropertyChanged(row.Index, newValue); + row.SetDetailsVisibilityInternal( + newValue, + true /*raiseNotification*/); + } + } + + /// + /// Gets or sets the row header. + /// + public object Header + { + get { return GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + /// + /// Identifies the Header dependency property. + /// + public static readonly DependencyProperty HeaderProperty = + DependencyProperty.Register( + "Header", + typeof(object), + typeof(DataGridRow), + new PropertyMetadata(null, OnHeaderPropertyChanged)); + + /// + /// HeaderProperty property changed handler. + /// + /// DataGridRow that changed its Header. + /// DependencyPropertyChangedEventArgs. + private static void OnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRow row = d as DataGridRow; + if (row._headerElement != null) + { + row._headerElement.Content = e.NewValue; + } + } + + /// + /// Gets or sets the style that is used when rendering the row header. + /// + public Style HeaderStyle + { + get { return GetValue(HeaderStyleProperty) as Style; } + set { SetValue(HeaderStyleProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HeaderStyleProperty = + DependencyProperty.Register( + "HeaderStyle", + typeof(Style), + typeof(DataGridRow), + new PropertyMetadata(null, OnHeaderStylePropertyChanged)); + + private static void OnHeaderStylePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRow row = d as DataGridRow; + if (row != null && row._headerElement != null) + { + row._headerElement.EnsureStyle(e.OldValue as Style); + } + } + + /// + /// Gets a value indicating whether the data in a row is valid. + /// + public bool IsValid + { + get + { + return (bool)GetValue(IsValidProperty); + } + + internal set + { + this.SetValueNoCallback(IsValidProperty, value); + } + } + + /// + /// Identifies the IsValid dependency property. + /// + public static readonly DependencyProperty IsValidProperty = + DependencyProperty.Register( + "IsValid", + typeof(bool), + typeof(DataGridRow), + new PropertyMetadata(true, OnIsValidPropertyChanged)); + + /// + /// IsValidProperty property changed handler. + /// + /// DataGridRow that changed its IsValid. + /// DependencyPropertyChangedEventArgs. + private static void OnIsValidPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRow dataGridRow = d as DataGridRow; + if (!dataGridRow.IsHandlerSuspended(e.Property)) + { + dataGridRow.SetValueNoCallback(DataGridRow.IsValidProperty, e.OldValue); + throw DataGridError.DataGrid.UnderlyingPropertyIsReadOnly("IsValid"); + } + } + + internal double ActualBottomGridLineHeight + { + get + { + if (_bottomGridLine != null && this.OwningGrid != null && this.OwningGrid.AreRowBottomGridLinesRequired) + { + // Unfortunately, _bottomGridLine has no size yet so we can't get its actual height + return DataGrid.HorizontalGridLinesThickness; + } + + return 0; + } + } + + internal DataGridCellCollection Cells + { + get; + private set; + } + + internal Brush ComputedForeground + { + get + { + return _computedForeground; + } + + set + { + if (_computedForeground != value) + { + _computedForeground = value; + + if (this.Cells != null) + { + foreach (DataGridCell dataGridCell in this.Cells) + { + FrameworkElement element = dataGridCell.Content as FrameworkElement; + if (element != null) + { + dataGridCell.OwningColumn.RefreshForeground(element, _computedForeground); + } + } + } + } + } + } + + internal double DetailsContentHeight + { + get + { + if (_detailsElement != null) + { + return _detailsElement.ContentHeight; + } + + return 0; + } + } + + internal DataGridCell FillerCell + { + get + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + if (_fillerCell == null) + { + _fillerCell = new DataGridCell(); + + Windows.UI.Xaml.Automation.AutomationProperties.SetAccessibilityView( + _fillerCell, + AccessibilityView.Raw); + + _fillerCell.Visibility = Visibility.Collapsed; + _fillerCell.OwningRow = this; + _fillerCell.EnsureStyle(null); + if (_cellsElement != null) + { + _cellsElement.Children.Add(_fillerCell); + } + } + + return _fillerCell; + } + } + + internal bool HasBottomGridLine + { + get + { + return _bottomGridLine != null; + } + } + + internal bool HasHeaderCell + { + get + { + return _headerElement != null; + } + } + + internal DataGridRowHeader HeaderCell + { + get + { + return _headerElement; + } + } + + /// + /// Gets or sets the index of the row. + /// + internal int Index + { + get; + set; + } + + internal bool IsEditing + { + get + { + return this.OwningGrid != null && this.OwningGrid.EditingRow == this; + } + } + + /// + /// Gets a value indicating whether the layout when template is applied. + /// + internal bool IsLayoutDelayed + { + get; + private set; + } + + internal bool IsPointerOver + { + get + { + return this.InteractionInfo != null && this.InteractionInfo.IsPointerOver; + } + + set + { + if (value && this.InteractionInfo == null) + { + this.InteractionInfo = new DataGridInteractionInfo(); + } + + if (this.InteractionInfo != null) + { + this.InteractionInfo.IsPointerOver = value; + } + + ApplyState(true /*animate*/); + } + } + + internal bool IsRecycled + { + get; + private set; + } + + internal bool IsRecyclable + { + get + { + if (this.OwningGrid != null) + { + return this.OwningGrid.IsRowRecyclable(this); + } + + return true; + } + } + + internal bool IsSelected + { + get + { + if (this.OwningGrid == null || this.Slot == -1) + { + // The Slot can be -1 if we're about to reuse or recycle this row, but the layout cycle has not + // passed so we don't know the outcome yet. We don't care whether or not it's selected in this case + return false; + } + + Debug.Assert(this.Index != -1, "Expected Index other than -1."); + return this.OwningGrid.GetRowSelection(this.Slot); + } + } + + internal DataGrid OwningGrid + { + get; + set; + } + + internal Panel RootElement + { + get; + private set; + } + + internal int Slot + { + get; + set; + } + + // Height that the row will eventually end up at after a possible details animation has completed + internal double TargetHeight + { + get + { + if (!double.IsNaN(this.Height)) + { + return this.Height; + } + else + { + this.EnsureMeasured(); + if (_detailsElement != null && _appliedDetailsVisibility == Visibility.Visible && _appliedDetailsTemplate != null) + { + Debug.Assert(!double.IsNaN(_detailsElement.ContentHeight), "Expected _detailsElement.ContentHeight different from double.NaN."); + Debug.Assert(!double.IsNaN(_detailsDesiredHeight), "Expected _detailsDesiredHeight different from double.NaN."); + return this.DesiredSize.Height + _detailsDesiredHeight - _detailsElement.ContentHeight; + } + else + { + return this.DesiredSize.Height; + } + } + } + } + + // Returns the actual template that should be sued for Details: either explicitly set on this row + // or inherited from the DataGrid + private DataTemplate ActualDetailsTemplate + { + get + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + DataTemplate currentDetailsTemplate = this.DetailsTemplate; + + return currentDetailsTemplate != null ? currentDetailsTemplate : this.OwningGrid.RowDetailsTemplate; + } + } + + private Visibility ActualDetailsVisibility + { + get + { + if (this.OwningGrid == null) + { + throw DataGridError.DataGrid.NoOwningGrid(this.GetType()); + } + + if (this.Index == -1) + { + throw DataGridError.DataGridRow.InvalidRowIndexCannotCompleteOperation(); + } + + return this.OwningGrid.GetRowDetailsVisibility(this.Index); + } + } + + private bool AreDetailsVisible + { + get + { + return this.DetailsVisibility == Visibility.Visible; + } + } + + private DataGridInteractionInfo InteractionInfo + { + get; + set; + } + + /// + /// Returns the row which contains the given element + /// + /// element contained in a row + /// Row that contains the element, or null if not found + /// + public static DataGridRow GetRowContainingElement(FrameworkElement element) + { + // Walk up the tree to find the DataGridRow that contains the element + DependencyObject parent = element; + DataGridRow row = parent as DataGridRow; + while (parent != null && row == null) + { + parent = VisualTreeHelper.GetParent(parent); + row = parent as DataGridRow; + } + + return row; + } + + /// + /// Returns the index of the current row. + /// + /// + /// The index of the current row. + /// + public int GetIndex() + { + return this.Index; + } + + /// + /// Arranges the content of the . + /// + /// + /// The actual size used by the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + // If the DataGrid was scrolled horizontally after our last Arrange, we need to make sure + // the Cells and Details are Arranged again + if (_lastHorizontalOffset != this.OwningGrid.HorizontalOffset) + { + _lastHorizontalOffset = this.OwningGrid.HorizontalOffset; + InvalidateHorizontalArrange(); + } + + Size size = base.ArrangeOverride(finalSize); + + if (_checkDetailsContentHeight) + { + _checkDetailsContentHeight = false; + EnsureDetailsContentHeight(); + } + + if (this.RootElement != null) + { + foreach (UIElement child in this.RootElement.Children) + { + if (DataGridFrozenGrid.GetIsFrozen(child)) + { + TranslateTransform transform = new TranslateTransform(); + + // Automatic layout rounding doesn't apply to transforms so we need to Round this + transform.X = Math.Round(this.OwningGrid.HorizontalOffset); + child.RenderTransform = transform; + } + } + } + + if (_bottomGridLine != null) + { + RectangleGeometry gridlineClipGeometry = new RectangleGeometry(); + gridlineClipGeometry.Rect = new Rect(this.OwningGrid.HorizontalOffset, 0, Math.Max(0, this.DesiredSize.Width - this.OwningGrid.HorizontalOffset), _bottomGridLine.DesiredSize.Height); + _bottomGridLine.Clip = gridlineClipGeometry; + } + + return size; + } + + /// + /// Measures the children of a to + /// prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (this.OwningGrid == null) + { + return base.MeasureOverride(availableSize); + } + + // Allow the DataGrid specific components to adjust themselves based on new values + if (_headerElement != null) + { + _headerElement.InvalidateMeasure(); + } + + if (_cellsElement != null) + { + _cellsElement.InvalidateMeasure(); + } + + if (_detailsElement != null) + { + _detailsElement.InvalidateMeasure(); + } + + bool currentAddItemIsDataContext = false; +#if FEATURE_IEDITABLECOLLECTIONVIEW + currentAddItemIsDataContext = this.OwningGrid.DataConnection.EditableCollectionView.CurrentAddItem == this.DataContext; +#endif + Size desiredSize = default; + try + { + desiredSize = base.MeasureOverride(availableSize); + } + catch + { + } + + desiredSize.Width = Math.Max(desiredSize.Width, this.OwningGrid.CellsWidth); + if (double.IsNaN(this.Height) && DoubleUtil.IsZero(this.MinHeight) && + (this.Index == this.OwningGrid.DataConnection.NewItemPlaceholderIndex || + (this.OwningGrid.DataConnection.IsAddingNew && currentAddItemIsDataContext))) + { + // In order to avoid auto-sizing the placeholder or new item row to an unusable height, we will + // measure it at the DataGrid's average RowHeightEstimate if its Height has not been explicitly set. + desiredSize.Height = Math.Max(desiredSize.Height, this.OwningGrid.RowHeightEstimate); + } + + return desiredSize; + } + + /// + /// Builds the visual tree for the column header when a new template is applied. + /// + protected override void OnApplyTemplate() + { + this.RootElement = GetTemplateChild(DATAGRIDROW_elementRoot) as Panel; + + if (this.RootElement != null) + { + EnsureBackground(); + ApplyState(false /*animate*/); + } + + if (_cellsElement != null) + { + // If we're applying a new template, we want to remove the cells from the previous _cellsElement + _cellsElement.Children.Clear(); + } + + _cellsElement = GetTemplateChild(DATAGRIDROW_elementCells) as DataGridCellsPresenter; + if (_cellsElement != null) + { + _cellsElement.OwningRow = this; + + // Cells that were already added before the Template was applied need to + // be added to the Canvas + if (this.Cells.Count > 0) + { + foreach (DataGridCell cell in this.Cells) + { + _cellsElement.Children.Add(cell); + } + } + } + + _detailsElement = GetTemplateChild(DATAGRIDROW_elementDetails) as DataGridDetailsPresenter; + if (_detailsElement != null && this.OwningGrid != null) + { + _detailsElement.OwningRow = this; + if (this.ActualDetailsVisibility == Visibility.Visible && this.ActualDetailsTemplate != null && _appliedDetailsTemplate == null) + { + // Apply the DetailsTemplate now that the row template is applied. + SetDetailsVisibilityInternal( + this.ActualDetailsVisibility, + _detailsVisibilityNotificationPending /*raiseNotification*/); + _detailsVisibilityNotificationPending = false; + } + } + + _bottomGridLine = GetTemplateChild(DATAGRIDROW_elementBottomGridLine) as Rectangle; + + _headerElement = GetTemplateChild(DATAGRIDROW_elementRowHeader) as DataGridRowHeader; + if (_headerElement != null) + { + _headerElement.Owner = this; + if (this.Header != null) + { + _headerElement.Content = Header; + } + + EnsureHeaderStyleAndVisibility(null); + } + + EnsureGridLines(); + EnsureForeground(); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridRowAutomationPeer(this); + } + + internal void ApplyCellsState(bool animate) + { + foreach (DataGridCell dataGridCell in this.Cells) + { + dataGridCell.ApplyCellState(animate); + } + } + + internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) + { + if (_detailsElement != null && this.AreDetailsVisible) + { + DataTemplate oldDetailsTemplate = _appliedDetailsTemplate; + if (this.ActualDetailsTemplate != _appliedDetailsTemplate) + { + if (this.ActualDetailsTemplate == null) + { + UnloadDetailsTemplate(false /*recycle*/, false /*setDetailsVisibilityToCollapsed*/); + } + else + { + if (_detailsContent != null) + { + _detailsContent.SizeChanged -= new SizeChangedEventHandler(DetailsContent_SizeChanged); + if (_detailsLoaded) + { + this.OwningGrid.OnUnloadingRowDetails(this, _detailsContent); + _detailsLoaded = false; + } + } + + _detailsElement.Children.Clear(); + + _detailsContent = this.ActualDetailsTemplate.LoadContent() as FrameworkElement; + _appliedDetailsTemplate = this.ActualDetailsTemplate; + + if (_detailsContent != null) + { + _detailsContent.SizeChanged += new SizeChangedEventHandler(DetailsContent_SizeChanged); + _detailsElement.Children.Add(_detailsContent); + } + } + } + + if (_detailsContent != null && !_detailsLoaded) + { + _detailsLoaded = true; + _detailsContent.DataContext = this.DataContext; + this.OwningGrid.OnLoadingRowDetails(this, _detailsContent); + } + + if (initializeDetailsPreferredHeight && double.IsNaN(_detailsDesiredHeight) && + _appliedDetailsTemplate != null && _detailsElement.Children.Count > 0) + { + EnsureDetailsDesiredHeight(); + } + else if (oldDetailsTemplate == null) + { + _detailsDesiredHeight = double.NaN; + EnsureDetailsDesiredHeight(); + _detailsElement.ContentHeight = _detailsDesiredHeight; + } + } + } + + internal void ApplyHeaderState(bool animate) + { + if (_headerElement != null && this.OwningGrid.AreRowHeadersVisible) + { + _headerElement.ApplyOwnerState(animate); + } + } + + /// + /// Updates the background brush of the row, using a storyboard if available. + /// + internal void ApplyState(bool animate) + { + if (this.RootElement != null && this.OwningGrid != null && this.Visibility == Visibility.Visible) + { + Debug.Assert(this.Index != -1, "Expected Index other than -1."); + byte idealStateMappingIndex = 0; + if (this.IsSelected || this.IsEditing) + { + idealStateMappingIndex += 8; + } + + if (this.IsEditing) + { + idealStateMappingIndex += 4; + } + + if (this.IsPointerOver) + { + idealStateMappingIndex += 2; + } + + if (this.OwningGrid.ContainsFocus) + { + idealStateMappingIndex += 1; + } + + byte stateCode = _idealStateMapping[idealStateMappingIndex]; + Debug.Assert(stateCode != DATAGRIDROW_stateNullCode, "stateCode other than DATAGRIDROW_stateNullCode."); + + string storyboardName; + while (stateCode != DATAGRIDROW_stateNullCode) + { + if (stateCode == DATAGRIDROW_stateNormalCode) + { + if (this.Index % 2 == 1) + { + storyboardName = DATAGRIDROW_stateAlternate; + } + else + { + storyboardName = DATAGRIDROW_stateNormal; + } + } + else + { + storyboardName = _stateNames[stateCode]; + } + + if (VisualStateManager.GoToState(this, storyboardName, animate)) + { + break; + } + else + { + // The state wasn't implemented so fall back to the next one. + stateCode = _fallbackStateMapping[stateCode]; + } + } + + if (this.IsValid) + { + VisualStates.GoToState(this, animate, VisualStates.StateValid); + } + else + { + VisualStates.GoToState(this, animate, VisualStates.StateInvalid, VisualStates.StateValid); + } + + ApplyHeaderState(animate); + } + } + + internal void DetachFromDataGrid(bool recycle) + { + UnloadDetailsTemplate(recycle, true /*setDetailsVisibilityToCollapsed*/); + + if (recycle) + { + Recycle(); + + if (_cellsElement != null) + { + _cellsElement.Recycle(); + } + + _checkDetailsContentHeight = false; + + // Clear out the old Details cache so it won't be reused for other data + _detailsDesiredHeight = double.NaN; + if (_detailsElement != null) + { + _detailsElement.ClearValue(DataGridDetailsPresenter.ContentHeightProperty); + } + } + + this.Slot = -1; + } + + // Make sure the row's background is set to its correct value. It could be explicitly set or inherit + // DataGrid.RowBackground or DataGrid.AlternatingRowBackground + internal void EnsureBackground() + { + // Inherit the DataGrid's RowBackground properties only if this row doesn't explicitly have a background set + if (this.RootElement != null && this.OwningGrid != null) + { + Debug.Assert(this.Index != -1, "Expected Index other than -1."); + + Brush newBackground = null; + if (this.Background == null) + { + if (this.Index % 2 == 0 || this.OwningGrid.AlternatingRowBackground == null) + { + // Use OwningGrid.RowBackground if the index is even or if the OwningGrid.AlternatingRowBackground is null + if (this.OwningGrid.RowBackground != null) + { + newBackground = this.OwningGrid.RowBackground; + } + } + else + { + // Alternate row + if (this.OwningGrid.AlternatingRowBackground != null) + { + newBackground = this.OwningGrid.AlternatingRowBackground; + } + } + } + else + { + newBackground = this.Background; + } + + if (this.RootElement.Background != newBackground) + { + this.RootElement.Background = newBackground; + } + } + } + + // Make sure the row's foreground is set to its correct value. It could be explicitly set or inherit + // DataGrid.RowForeground or DataGrid.AlternatingRowForeground + internal void EnsureForeground() + { + // Inherit the DataGrid's RowForeground properties only if this row doesn't explicitly have a foreground set + if (this.OwningGrid != null) + { + Debug.Assert(this.Index != -1, "Expected Index other than -1."); + + PropertyMetadata metadataInfo = DataGridRow.ForegroundProperty.GetMetadata(typeof(DataGridRow)); + Brush defaultForeground = metadataInfo == null ? null : metadataInfo.DefaultValue as Brush; + Brush newForeground = null; + + if (this.Foreground.Equals(defaultForeground)) + { + if (this.Index % 2 == 0 || this.OwningGrid.AlternatingRowForeground == null) + { + // Use OwningGrid.RowForeground if the index is even or if the OwningGrid.AlternatingRowForeground is null + if (this.OwningGrid.RowForeground != null) + { + newForeground = this.OwningGrid.RowForeground; + } + } + else + { + // Alternate row + if (this.OwningGrid.AlternatingRowForeground != null) + { + newForeground = this.OwningGrid.AlternatingRowForeground; + } + } + + if (newForeground == null) + { + newForeground = this.Foreground; + } + } + else + { + newForeground = this.Foreground; + } + + this.ComputedForeground = newForeground; + } + else + { + this.ComputedForeground = this.Foreground; + } + } + + internal void EnsureFillerVisibility() + { + if (_cellsElement != null) + { + _cellsElement.EnsureFillerVisibility(); + } + } + + internal void EnsureGridLines() + { + if (this.OwningGrid != null) + { + if (_bottomGridLine != null) + { + Visibility newVisibility = this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All + ? Visibility.Visible : Visibility.Collapsed; + + if (newVisibility != _bottomGridLine.Visibility) + { + _bottomGridLine.Visibility = newVisibility; + } + + EnsureHeaderGridLines(newVisibility); + + _bottomGridLine.Fill = this.OwningGrid.HorizontalGridLinesBrush; + } + + foreach (DataGridCell cell in this.Cells) + { + cell.EnsureGridLine(this.OwningGrid.ColumnsInternal.LastVisibleColumn); + } + } + } + + // Set the proper style for the Header by walking up the Style hierarchy + internal void EnsureHeaderStyleAndVisibility(Style previousStyle) + { + if (_headerElement != null && this.OwningGrid != null) + { + if (this.OwningGrid.AreRowHeadersVisible) + { + _headerElement.EnsureStyle(previousStyle); + _headerElement.Visibility = Visibility.Visible; + } + else + { + _headerElement.Visibility = Visibility.Collapsed; + } + } + } + + internal void EnsureHeaderVisibility() + { + if (_headerElement != null && this.OwningGrid != null) + { + _headerElement.Visibility = this.OwningGrid.AreRowHeadersVisible ? Visibility.Visible : Visibility.Collapsed; + } + } + + private void EnsureHeaderGridLines(Visibility visibility) + { + if (_headerElement != null) + { + _headerElement.SeparatorVisibility = visibility; + } + } + + internal void InvalidateHorizontalArrange() + { + if (_cellsElement != null) + { + _cellsElement.InvalidateArrange(); + } + + if (_detailsElement != null) + { + _detailsElement.InvalidateArrange(); + } + } + + // Sets AreDetailsVisible on the row and animates if necessary + internal void SetDetailsVisibilityInternal( + Visibility visibility, + bool raiseNotification) + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + Debug.Assert(this.Index != -1, "Expected Index other than -1."); + + if (_appliedDetailsVisibility != visibility) + { + if (_detailsElement == null) + { + if (raiseNotification) + { + _detailsVisibilityNotificationPending = true; + } + + return; + } + + _appliedDetailsVisibility = visibility; + this.SetValueNoCallback(DetailsVisibilityProperty, visibility); + + // Applies a new DetailsTemplate only if it has changed either here or at the DataGrid level + ApplyDetailsTemplate(true /*initializeDetailsPreferredHeight*/); + + // no template to show + if (_appliedDetailsTemplate == null) + { + if (_detailsElement.ContentHeight > 0) + { + _detailsElement.ContentHeight = 0; + } + + return; + } + + if (this.AreDetailsVisible) + { + // Set the details height directly + _detailsElement.ContentHeight = _detailsDesiredHeight; + _checkDetailsContentHeight = true; + } + else + { + _detailsElement.ContentHeight = 0; + } + + OnRowDetailsChanged(); + + if (raiseNotification) + { + this.OwningGrid.OnRowDetailsVisibilityChanged(new DataGridRowDetailsEventArgs(this, _detailsContent)); + } + } + } + + private void CancelPointer(PointerRoutedEventArgs e) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId == e.Pointer.PointerId) + { + this.InteractionInfo.CapturedPointerId = 0u; + } + + this.IsPointerOver = false; + } + + private void DataGridCellCollection_CellAdded(object sender, DataGridCellEventArgs e) + { + if (_cellsElement != null) + { + _cellsElement.Children.Add(e.Cell); + } + } + + private void DataGridCellCollection_CellRemoved(object sender, DataGridCellEventArgs e) + { + if (_cellsElement != null) + { + _cellsElement.Children.Remove(e.Cell); + } + } + + private void DataGridRow_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void DataGridRow_PointerCaptureLost(object sender, PointerRoutedEventArgs e) + { + CancelPointer(e); + } + + private void DataGridRow_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch && + this.OwningGrid != null && + this.OwningGrid.AllowsManipulation && + (this.InteractionInfo == null || this.InteractionInfo.CapturedPointerId == 0u) && + this.CapturePointer(e.Pointer)) + { + if (this.InteractionInfo == null) + { + this.InteractionInfo = new DataGridInteractionInfo(); + } + + this.InteractionInfo.CapturedPointerId = e.Pointer.PointerId; + } + } + + private void DataGridRow_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId == e.Pointer.PointerId) + { + ReleasePointerCapture(e.Pointer); + } + } + + private void DataGridRow_PointerEntered(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridRow_PointerExited(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(false); + } + + private void DataGridRow_PointerMoved(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridRow_Tapped(object sender, TappedRoutedEventArgs e) + { + if (this.OwningGrid != null && !this.OwningGrid.HasColumnUserInteraction) + { + if (this.OwningGrid.UpdatedStateOnTapped) + { + this.OwningGrid.UpdatedStateOnTapped = false; + } + else + { + e.Handled = this.OwningGrid.UpdateStateOnTapped(e, -1, this.Slot, false /*allowEdit*/); + } + } + } + + private void DetailsContent_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (e.NewSize.Height != e.PreviousSize.Height && e.NewSize.Height != _detailsDesiredHeight) + { + // Update the new desired height for RowDetails + _detailsDesiredHeight = e.NewSize.Height; + + if (this.AreDetailsVisible && _appliedDetailsTemplate != null) + { + _detailsElement.ContentHeight = e.NewSize.Height; + + // Calling this when details are not visible invalidates during layout when we have no work + // to do. In certain scenarios, this could cause a layout cycle + OnRowDetailsChanged(); + } + } + } + + // Makes sure the _detailsDesiredHeight is initialized. We need to measure it to know what + // height we want to animate to. Subsequently, we just update that height in response to SizeChanged. + private void EnsureDetailsDesiredHeight() + { + Debug.Assert(_detailsElement != null, "Expected non-null _detailsElement."); + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + + if (_detailsContent != null) + { + Debug.Assert(_detailsElement.Children.Contains(_detailsContent), "Expected _detailsElement parent of _detailsContent."); + + _detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + _detailsDesiredHeight = _detailsContent.DesiredSize.Height; + } + else + { + _detailsDesiredHeight = 0; + } + } + + private void EnsureDetailsContentHeight() + { + if (_detailsElement != null && + _detailsContent != null && + double.IsNaN(_detailsContent.Height) && + this.AreDetailsVisible && + !double.IsNaN(_detailsDesiredHeight) && + !DoubleUtil.AreClose(_detailsContent.ActualHeight, _detailsDesiredHeight) && + this.Slot != -1) + { + _detailsDesiredHeight = _detailsContent.ActualHeight; + _detailsElement.ContentHeight = _detailsDesiredHeight; + } + } + + private void OnDependencyPropertyChanged(DependencyObject dependencyObject, DependencyProperty dependencyProperty) + { + if (dependencyProperty == Control.ForegroundProperty) + { + EnsureForeground(); + } + } + + private void OnRowDetailsChanged() + { + if (this.OwningGrid != null) + { + this.OwningGrid.OnRowDetailsChanged(); + } + } + + private void Recycle() + { + this.InteractionInfo = null; + this.IsRecycled = true; + } + + private void UnloadDetailsTemplate(bool recycle, bool setDetailsVisibilityToCollapsed) + { + if (_detailsElement != null) + { + if (_detailsContent != null) + { + if (_detailsLoaded) + { + this.OwningGrid.OnUnloadingRowDetails(this, _detailsContent); + } + + _detailsContent.DataContext = null; + if (!recycle) + { + _detailsContent.SizeChanged -= new SizeChangedEventHandler(DetailsContent_SizeChanged); + _detailsContent = null; + } + } + + if (!recycle) + { + _detailsElement.Children.Clear(); + } + + _detailsElement.ContentHeight = 0; + } + + if (!recycle) + { + _appliedDetailsTemplate = null; + this.SetValueNoCallback(DetailsTemplateProperty, null); + } + + _detailsLoaded = false; + _appliedDetailsVisibility = null; + + if (setDetailsVisibilityToCollapsed) + { + this.SetValueNoCallback(DetailsVisibilityProperty, Visibility.Collapsed); + } + } + + private void UpdateIsPointerOver(bool isPointerOver) + { + if (this.InteractionInfo != null && this.InteractionInfo.CapturedPointerId != 0u) + { + return; + } + + this.IsPointerOver = isPointerOver; + } + +#if DEBUG + /// + /// Gets the row's Index. + /// + public int Debug_Index + { + get + { + return this.Index; + } + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowClipboardEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowClipboardEventArgs.cs new file mode 100644 index 0000000..8accf42 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowClipboardEventArgs.cs @@ -0,0 +1,68 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// This class encapsulates a selected row's information necessary for the CopyingRowClipboardContent event. + /// + public class DataGridRowClipboardEventArgs : EventArgs + { + private List _clipboardRowContent; + private bool _isColumnHeadersRow; + private object _item; + + /// + /// Initializes a new instance of the class. + /// + /// The row's associated data item. + /// Whether or not this EventArgs is for the column headers. + internal DataGridRowClipboardEventArgs(object item, bool isColumnHeadersRow) + { + _isColumnHeadersRow = isColumnHeadersRow; + _item = item; + } + + /// + /// Gets a list used to modify, add or remove a cell content before it gets stored into the clipboard. + /// + public List ClipboardRowContent + { + get + { + if (_clipboardRowContent == null) + { + _clipboardRowContent = new List(); + } + + return _clipboardRowContent; + } + } + + /// + /// Gets a value indicating whether this property is true when the ClipboardRowContent represents column headers, in which case the Item is null. + /// + public bool IsColumnHeadersRow + { + get + { + return _isColumnHeadersRow; + } + } + + /// + /// Gets the row item used for preparing the ClipboardRowContent. + /// + public object Item + { + get + { + return _item; + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowDetailsEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowDetailsEventArgs.cs new file mode 100644 index 0000000..212b03a --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowDetailsEventArgs.cs @@ -0,0 +1,45 @@ +// 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; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for the , , + /// and events. + /// + public class DataGridRowDetailsEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The row that the event occurs for. + /// The row details section as a framework element. + public DataGridRowDetailsEventArgs(DataGridRow row, FrameworkElement detailsElement) + { + this.Row = row; + this.DetailsElement = detailsElement; + } + + /// + /// Gets the row details section as a framework element. + /// + public FrameworkElement DetailsElement + { + get; + private set; + } + + /// + /// Gets the row that the event occurs for. + /// + public DataGridRow Row + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndedEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndedEventArgs.cs new file mode 100644 index 0000000..e81d373 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndedEventArgs.cs @@ -0,0 +1,43 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides information just after a row has exited edit mode. + /// + public class DataGridRowEditEndedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The row container of the cell container that has just exited edit mode. + /// The editing action that has been taken. + public DataGridRowEditEndedEventArgs(DataGridRow row, DataGridEditAction editAction) + { + this.Row = row; + this.EditAction = editAction; + } + + /// + /// Gets the editing action that has been taken. + /// + public DataGridEditAction EditAction + { + get; + private set; + } + + /// + /// Gets the row container of the cell container that has just exited edit mode. + /// + public DataGridRow Row + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndingEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndingEventArgs.cs new file mode 100644 index 0000000..264e91f --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEditEndingEventArgs.cs @@ -0,0 +1,43 @@ +// 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.ComponentModel; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides information just before a row exits editing mode. + /// + public class DataGridRowEditEndingEventArgs : CancelEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The row container of the cell container that is about to exit edit mode. + /// The editing action that will be taken. + public DataGridRowEditEndingEventArgs(DataGridRow row, DataGridEditAction editAction) + { + this.Row = row; + this.EditAction = editAction; + } + + /// + /// Gets the editing action that will be taken. + /// + public DataGridEditAction EditAction + { + get; + private set; + } + + /// + /// Gets the row container of the cell container that is about to exit edit mode. + /// + public DataGridRow Row + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEventArgs.cs new file mode 100644 index 0000000..bf56520 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowEventArgs.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Provides data for row-related events. + /// + public class DataGridRowEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The row that the event occurs for. + public DataGridRowEventArgs(DataGridRow dataGridRow) + { + this.Row = dataGridRow; + } + + /// + /// Gets the row that the event occurs for. + /// + public DataGridRow Row + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeader.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeader.cs new file mode 100644 index 0000000..93ebea1 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeader.cs @@ -0,0 +1,749 @@ +// 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; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Controls.Primitives; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Shapes; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents the header of a row group. + /// + [TemplatePart(Name = DataGridRow.DATAGRIDROW_elementRoot, Type = typeof(Panel))] + [TemplatePart(Name = DataGridRow.DATAGRIDROW_elementRowHeader, Type = typeof(DataGridRowHeader))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_bottomGridLine, Type = typeof(Rectangle))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_expanderButton, Type = typeof(ToggleButton))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_indentSpacer, Type = typeof(FrameworkElement))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_itemCountElement, Type = typeof(TextBlock))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_propertyNameElement, Type = typeof(TextBlock))] + [TemplatePart(Name = DATAGRIDROWGROUPHEADER_propertyValueElement, Type = typeof(TextBlock))] + [StyleTypedProperty(Property = "HeaderStyle", StyleTargetType = typeof(DataGridRowHeader))] + public class DataGridRowGroupHeader : Control + { + private const string DATAGRIDROWGROUPHEADER_bottomGridLine = "BottomGridLine"; + private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; + private const string DATAGRIDROWGROUPHEADER_indentSpacer = "IndentSpacer"; + private const string DATAGRIDROWGROUPHEADER_itemCountElement = "ItemCountElement"; + private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PropertyNameElement"; + private const string DATAGRIDROWGROUPHEADER_propertyValueElement = "PropertyValueElement"; + + private bool _areIsCheckedHandlersSuspended; + private Rectangle _bottomGridLine; + private ToggleButton _expanderButton; + private FrameworkElement _indentSpacer; + private TextBlock _itemCountElement; + private TextBlock _propertyNameElement; + private TextBlock _propertyValueElement; + private Panel _rootElement; + private double _totalIndent; + + /// + /// Initializes a new instance of the class. + /// + public DataGridRowGroupHeader() + { + DefaultStyleKey = typeof(DataGridRowGroupHeader); + + this.AddHandler(UIElement.TappedEvent, new TappedEventHandler(DataGridRowGroupHeader_Tapped), true /*handledEventsToo*/); + this.AddHandler(UIElement.DoubleTappedEvent, new DoubleTappedEventHandler(DataGridRowGroupHeader_DoubleTapped), true /*handledEventsToo*/); + + this.PointerCanceled += new PointerEventHandler(DataGridRowGroupHeader_PointerCanceled); + this.PointerEntered += new PointerEventHandler(DataGridRowGroupHeader_PointerEntered); + this.PointerExited += new PointerEventHandler(DataGridRowGroupHeader_PointerExited); + this.PointerMoved += new PointerEventHandler(DataGridRowGroupHeader_PointerMoved); + this.PointerPressed += new PointerEventHandler(DataGridRowGroupHeader_PointerPressed); + this.PointerReleased += new PointerEventHandler(DataGridRowGroupHeader_PointerReleased); + } + + /// + /// Gets or sets the style applied to the header cell of a . + /// + public Style HeaderStyle + { + get { return GetValue(HeaderStyleProperty) as Style; } + set { SetValue(HeaderStyleProperty, value); } + } + + /// + /// Dependency Property for HeaderStyle + /// + public static readonly DependencyProperty HeaderStyleProperty = + DependencyProperty.Register( + "HeaderStyle", + typeof(Style), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(null, OnHeaderStylePropertyChanged)); + + private static void OnHeaderStylePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRowGroupHeader groupHeader = d as DataGridRowGroupHeader; + if (groupHeader.HeaderElement != null) + { + groupHeader.HeaderElement.EnsureStyle(e.OldValue as Style); + } + } + + /// + /// Gets or sets a value that indicates whether the item count is visible. + /// + public Visibility ItemCountVisibility + { + get { return (Visibility)GetValue(ItemCountVisibilityProperty); } + set { SetValue(ItemCountVisibilityProperty, value); } + } + + /// + /// DependencyProperty for ItemCountVisibility + /// + public static readonly DependencyProperty ItemCountVisibilityProperty = + DependencyProperty.Register( + "ItemCountVisibility", + typeof(Visibility), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(Visibility.Visible)); + + /// + /// Gets the nesting level of the associated group. + /// + public int Level + { + get { return (int)GetValue(LevelProperty); } + internal set { SetValue(LevelProperty, value); } + } + + /// + /// Identifies the Level dependency property. + /// + public static readonly DependencyProperty LevelProperty = + DependencyProperty.Register( + "Level", + typeof(int), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(0)); + + /// + /// Gets or sets the name of the property that this row is bound to. + /// + public string PropertyName + { + get { return GetValue(PropertyNameProperty) as string; } + set { SetValue(PropertyNameProperty, value); } + } + + /// + /// DependencyProperty for PropertyName + /// + public static readonly DependencyProperty PropertyNameProperty = + DependencyProperty.Register( + "PropertyName", + typeof(string), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(null, OnPropertyNameChanged)); + + private static void OnPropertyNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRowGroupHeader groupHeader = d as DataGridRowGroupHeader; + groupHeader.UpdateTitleElements(); + } + + /// + /// Gets or sets a value that indicates whether the property name is visible. + /// + public Visibility PropertyNameVisibility + { + get { return (Visibility)GetValue(PropertyNameVisibilityProperty); } + set { SetValue(PropertyNameVisibilityProperty, value); } + } + + /// + /// DependencyProperty for PropertyNameVisibility + /// + public static readonly DependencyProperty PropertyNameVisibilityProperty = + DependencyProperty.Register( + "PropertyNameVisibility", + typeof(Visibility), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(Visibility.Visible)); + + /// + /// Gets or sets the value of the property that this row is bound to. + /// + public string PropertyValue + { + get { return GetValue(PropertyValueProperty) as string; } + set { SetValue(PropertyValueProperty, value); } + } + + /// + /// DependencyProperty for PropertyName + /// + public static readonly DependencyProperty PropertyValueProperty = + DependencyProperty.Register( + "PropertyValue", + typeof(string), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(null, OnPropertyValueChanged)); + + private static void OnPropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRowGroupHeader groupHeader = d as DataGridRowGroupHeader; + groupHeader.UpdateTitleElements(); + } + + /// + /// Gets or sets a value that indicates the amount that the + /// children of the are indented. + /// + public double SublevelIndent + { + get { return (double)GetValue(SublevelIndentProperty); } + set { SetValue(SublevelIndentProperty, value); } + } + + /// + /// SublevelIndent Dependency property + /// + public static readonly DependencyProperty SublevelIndentProperty = + DependencyProperty.Register( + "SublevelIndent", + typeof(double), + typeof(DataGridRowGroupHeader), + new PropertyMetadata(DataGrid.DATAGRID_defaultRowGroupSublevelIndent, OnSublevelIndentPropertyChanged)); + + private static void OnSublevelIndentPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridRowGroupHeader groupHeader = d as DataGridRowGroupHeader; + double newValue = (double)e.NewValue; + + // We don't need to revert to the old value if our input is bad because we never read this property value + if (double.IsNaN(newValue)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToNAN("SublevelIndent"); + } + else if (double.IsInfinity(newValue)) + { + throw DataGridError.DataGrid.ValueCannotBeSetToInfinity("SublevelIndent"); + } + else if (newValue < 0) + { + throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo("value", "SublevelIndent", 0); + } + + if (groupHeader.OwningGrid != null) + { + groupHeader.OwningGrid.OnSublevelIndentUpdated(groupHeader, newValue); + } + } + + /// + /// Gets the ICollectionViewGroup implementation associated with this . + /// + public ICollectionViewGroup CollectionViewGroup + { + get + { + return this.RowGroupInfo == null ? null : this.RowGroupInfo.CollectionViewGroup; + } + } + + internal DataGridRowHeader HeaderCell + { + get + { + return this.HeaderElement; + } + } + + private DataGridRowHeader HeaderElement + { + get; + set; + } + + private bool IsCurrent + { + get + { + Debug.Assert(this.OwningGrid != null, "Expected non-null OwningGrid."); + return this.RowGroupInfo.Slot == this.OwningGrid.CurrentSlot; + } + } + + private bool IsPointerOver + { + get; + set; + } + + private bool IsPressed + { + get; + set; + } + + internal bool IsRecycled + { + get; + set; + } + + internal DataGrid OwningGrid + { + get; + set; + } + + internal DataGridRowGroupInfo RowGroupInfo + { + get; + set; + } + + internal double TotalIndent + { + set + { + _totalIndent = value; + if (_indentSpacer != null) + { + _indentSpacer.Width = _totalIndent; + } + } + } + + internal void ApplyHeaderState(bool animate) + { + if (this.HeaderElement != null && this.OwningGrid.AreRowHeadersVisible) + { + this.HeaderElement.ApplyOwnerState(animate); + } + } + + internal void ApplyState(bool useTransitions) + { + // Common States + if (this.IsPressed) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StatePressed, VisualStates.StatePointerOver, VisualStates.StateNormal); + } + else if (this.IsPointerOver) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StatePointerOver, VisualStates.StateNormal); + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateNormal); + } + + // Current States + if (this.IsCurrent && !this.OwningGrid.ColumnHeaderHasFocus) + { + if (this.OwningGrid.ContainsFocus) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateCurrentWithFocus, VisualStates.StateCurrent, VisualStates.StateRegular); + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateCurrent, VisualStates.StateRegular); + } + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateRegular); + } + + // Expanded States + if (this.RowGroupInfo.CollectionViewGroup.GroupItems.Count == 0) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateEmpty); + } + else + { + if (this.RowGroupInfo.Visibility == Visibility.Visible) + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateExpanded, VisualStates.StateEmpty); + } + else + { + VisualStates.GoToState(this, useTransitions, VisualStates.StateCollapsed, VisualStates.StateEmpty); + } + } + } + + /// + /// ArrangeOverride + /// + /// The final area within the parent that this object should use to arrange itself and its children. + /// The actual size that is used after the element is arranged in layout. + protected override Size ArrangeOverride(Size finalSize) + { + if (this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + Size size = base.ArrangeOverride(finalSize); + if (_rootElement != null) + { + if (this.OwningGrid.AreRowGroupHeadersFrozen) + { + foreach (UIElement child in _rootElement.Children) + { + child.Clip = null; + } + } + else + { + double frozenLeftEdge = 0; + foreach (UIElement child in _rootElement.Children) + { + if (DataGridFrozenGrid.GetIsFrozen(child) && child.Visibility == Visibility.Visible) + { + TranslateTransform transform = new TranslateTransform(); + + // Automatic layout rounding doesn't apply to transforms so we need to Round this + transform.X = Math.Round(this.OwningGrid.HorizontalOffset); + child.RenderTransform = transform; + + double childLeftEdge = child.Translate(this, new Point(child.RenderSize.Width, 0)).X - transform.X; + frozenLeftEdge = Math.Max(frozenLeftEdge, childLeftEdge + this.OwningGrid.HorizontalOffset); + } + } + + // Clip the non-frozen elements so they don't overlap the frozen ones + foreach (UIElement child in _rootElement.Children) + { + if (!DataGridFrozenGrid.GetIsFrozen(child)) + { + EnsureChildClip(child, frozenLeftEdge); + } + } + } + } + + return size; + } + + internal void ClearFrozenStates() + { + if (_rootElement != null) + { + foreach (UIElement child in _rootElement.Children) + { + child.RenderTransform = null; + } + } + } + + private void DataGridRowGroupHeader_Tapped(object sender, TappedRoutedEventArgs e) + { + if (this.OwningGrid != null && !this.OwningGrid.HasColumnUserInteraction) + { + if (!e.Handled && this.OwningGrid.IsTabStop) + { + bool success = this.OwningGrid.Focus(FocusState.Programmatic); + Debug.Assert(success, "Expected successful focus change."); + } + + e.Handled = this.OwningGrid.UpdateStateOnTapped(e, this.OwningGrid.CurrentColumnIndex, this.RowGroupInfo.Slot, false /*allowEdit*/); + } + } + + private void DataGridRowGroupHeader_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) + { + if (this.OwningGrid != null && !this.OwningGrid.HasColumnUserInteraction && !e.Handled) + { + ToggleExpandCollapse(this.RowGroupInfo.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible, true); + e.Handled = true; + } + } + + private void EnsureChildClip(UIElement child, double frozenLeftEdge) + { + double childLeftEdge = child.Translate(this, new Point(0, 0)).X; + if (frozenLeftEdge > childLeftEdge) + { + double xClip = Math.Round(frozenLeftEdge - childLeftEdge); + RectangleGeometry rg = new RectangleGeometry(); + rg.Rect = new Rect(xClip, 0, Math.Max(0, child.RenderSize.Width - xClip), child.RenderSize.Height); + child.Clip = rg; + } + else + { + child.Clip = null; + } + } + + internal void EnsureExpanderButtonIsChecked() + { + if (_expanderButton != null && + this.RowGroupInfo != null && + this.RowGroupInfo.CollectionViewGroup != null && + this.RowGroupInfo.CollectionViewGroup.GroupItems != null && + this.RowGroupInfo.CollectionViewGroup.GroupItems.Count != 0) + { + SetIsCheckedNoCallBack(this.RowGroupInfo.Visibility == Visibility.Visible); + } + } + + internal void EnsureHeaderStyleAndVisibility(Style previousStyle) + { + if (this.HeaderElement != null && this.OwningGrid != null) + { + if (this.OwningGrid.AreRowHeadersVisible) + { + this.HeaderElement.EnsureStyle(previousStyle); + this.HeaderElement.Visibility = Visibility.Visible; + } + else + { + this.HeaderElement.Visibility = Visibility.Collapsed; + } + } + } + + private void ExpanderButton_Checked(object sender, RoutedEventArgs e) + { + if (!_areIsCheckedHandlersSuspended) + { + ToggleExpandCollapse(Visibility.Visible, true); + } + } + + private void ExpanderButton_Unchecked(object sender, RoutedEventArgs e) + { + if (!_areIsCheckedHandlersSuspended) + { + ToggleExpandCollapse(Visibility.Collapsed, true); + } + } + + internal void LoadVisualsForDisplay() + { + EnsureExpanderButtonIsChecked(); + + EnsureHeaderStyleAndVisibility(null); + ApplyState(false /*useTransitions*/); + ApplyHeaderState(false); + } + + /// + /// Builds the visual tree for the row group header when a new template is applied. + /// + protected override void OnApplyTemplate() + { + _rootElement = GetTemplateChild(DataGridRow.DATAGRIDROW_elementRoot) as Panel; + + if (_expanderButton != null) + { + _expanderButton.Checked -= ExpanderButton_Checked; + _expanderButton.Unchecked -= ExpanderButton_Unchecked; + } + + _bottomGridLine = GetTemplateChild(DATAGRIDROWGROUPHEADER_bottomGridLine) as Rectangle; + + _expanderButton = GetTemplateChild(DATAGRIDROWGROUPHEADER_expanderButton) as ToggleButton; + if (_expanderButton != null) + { + EnsureExpanderButtonIsChecked(); + _expanderButton.Checked += new RoutedEventHandler(ExpanderButton_Checked); + _expanderButton.Unchecked += new RoutedEventHandler(ExpanderButton_Unchecked); + } + + this.HeaderElement = GetTemplateChild(DataGridRow.DATAGRIDROW_elementRowHeader) as DataGridRowHeader; + if (this.HeaderElement != null) + { + this.HeaderElement.Owner = this; + EnsureHeaderStyleAndVisibility(null); + } + + _indentSpacer = GetTemplateChild(DATAGRIDROWGROUPHEADER_indentSpacer) as FrameworkElement; + if (_indentSpacer != null) + { + _indentSpacer.Width = _totalIndent; + } + + _itemCountElement = GetTemplateChild(DATAGRIDROWGROUPHEADER_itemCountElement) as TextBlock; + _propertyNameElement = GetTemplateChild(DATAGRIDROWGROUPHEADER_propertyNameElement) as TextBlock; + _propertyValueElement = GetTemplateChild(DATAGRIDROWGROUPHEADER_propertyValueElement) as TextBlock; + UpdateTitleElements(); + EnsureGridLine(); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridRowGroupHeaderAutomationPeer(this); + } + + private void SetIsCheckedNoCallBack(bool value) + { + if (_expanderButton != null && _expanderButton.IsChecked != value) + { + _areIsCheckedHandlersSuspended = true; + try + { + _expanderButton.IsChecked = value; + } + finally + { + _areIsCheckedHandlersSuspended = false; + } + } + } + + internal void ToggleExpandCollapse(Visibility newVisibility, bool setCurrent) + { + if (this.RowGroupInfo.CollectionViewGroup.GroupItems.Count != 0) + { + if (this.OwningGrid == null) + { + // Do these even if the OwningGrid is null in case it could improve the Designer experience for a standalone DataGridRowGroupHeader + this.RowGroupInfo.Visibility = newVisibility; + } + else + { + this.OwningGrid.OnRowGroupHeaderToggled(this, newVisibility, setCurrent); + } + + EnsureExpanderButtonIsChecked(); + ApplyState(true /*useTransitions*/); + } + } + + internal void UpdateTitleElements() + { + string propertyName = this.PropertyName; + bool hasPropertyValue = _propertyValueElement != null && !string.IsNullOrEmpty(this.PropertyValue); + + if (_propertyNameElement != null) + { + if (!string.IsNullOrWhiteSpace(propertyName) && this.OwningGrid.DataConnection.DataType != null) + { + string displayName = this.OwningGrid.DataConnection.DataType.GetDisplayName(propertyName); + if (!string.IsNullOrWhiteSpace(displayName)) + { + propertyName = displayName; + } + } + + if (string.IsNullOrEmpty(propertyName)) + { + propertyName = this.OwningGrid.RowGroupHeaderPropertyNameAlternative; + } + + if (!string.IsNullOrEmpty(propertyName) && hasPropertyValue) + { + propertyName = string.Format(CultureInfo.CurrentCulture, Properties.Resources.DataGridRowGroupHeader_PropertyName, propertyName); + } + + if (!string.IsNullOrEmpty(propertyName)) + { + _propertyNameElement.Text = propertyName; + } + } + + if (hasPropertyValue) + { + _propertyValueElement.Text = this.PropertyValue; + } + + if (_itemCountElement != null && this.RowGroupInfo != null && this.RowGroupInfo.CollectionViewGroup != null) + { + _itemCountElement.Text = string.Format( + CultureInfo.CurrentCulture, + this.RowGroupInfo.CollectionViewGroup.GroupItems.Count == 1 ? Properties.Resources.DataGridRowGroupHeader_ItemCountSingular : Properties.Resources.DataGridRowGroupHeader_ItemCountPlural, + this.RowGroupInfo.CollectionViewGroup.GroupItems.Count); + } + } + + private void DataGridRowGroupHeader_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(false); + UpdateIsPressed(false); + } + + private void DataGridRowGroupHeader_PointerEntered(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridRowGroupHeader_PointerExited(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(false); + } + + private void DataGridRowGroupHeader_PointerMoved(object sender, PointerRoutedEventArgs e) + { + UpdateIsPointerOver(true); + } + + private void DataGridRowGroupHeader_PointerPressed(object sender, PointerRoutedEventArgs e) + { + UpdateIsPressed(true); + } + + private void DataGridRowGroupHeader_PointerReleased(object sender, PointerRoutedEventArgs e) + { + UpdateIsPressed(false); + } + + internal void EnsureGridLine() + { + if (this.OwningGrid != null && _bottomGridLine != null) + { + Visibility newVisibility = this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || this.OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All + ? Visibility.Visible : Visibility.Collapsed; + + if (newVisibility != _bottomGridLine.Visibility) + { + _bottomGridLine.Visibility = newVisibility; + } + + _bottomGridLine.Fill = this.OwningGrid.HorizontalGridLinesBrush; + } + } + + private void UpdateIsPointerOver(bool isPointerOver) + { + if (!this.IsEnabled || isPointerOver == this.IsPointerOver) + { + return; + } + + this.IsPointerOver = isPointerOver; + ApplyState(true /*useTransitions*/); + } + + private void UpdateIsPressed(bool isPressed) + { + if (!this.IsEnabled || isPressed == this.IsPressed) + { + return; + } + + this.IsPressed = isPressed; + ApplyState(true /*useTransitions*/); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeaderEventArgs.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeaderEventArgs.cs new file mode 100644 index 0000000..7b4f274 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupHeaderEventArgs.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// EventArgs used for the DataGrid's LoadingRowGroup and UnloadingRowGroup events + /// + public class DataGridRowGroupHeaderEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The row group header that the event occurs for. + public DataGridRowGroupHeaderEventArgs(DataGridRowGroupHeader rowGroupHeader) + { + this.RowGroupHeader = rowGroupHeader; + } + + /// + /// Gets the associated with this instance. + /// + public DataGridRowGroupHeader RowGroupHeader + { + get; + private set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupInfo.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupInfo.cs new file mode 100644 index 0000000..8c2b588 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowGroupInfo.cs @@ -0,0 +1,56 @@ +// 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 Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridRowGroupInfo + { + public DataGridRowGroupInfo( + ICollectionViewGroup collectionViewGroup, + Visibility visibility, + int level, + int slot, + int lastSubItemSlot) + { + this.CollectionViewGroup = collectionViewGroup; + this.Visibility = visibility; + this.Level = level; + this.Slot = slot; + this.LastSubItemSlot = lastSubItemSlot; + } + + public ICollectionViewGroup CollectionViewGroup + { + get; + private set; + } + + public int LastSubItemSlot + { + get; + set; + } + + public int Level + { + get; + private set; + } + + public int Slot + { + get; + set; + } + + public Visibility Visibility + { + get; + set; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowHeader.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowHeader.cs new file mode 100644 index 0000000..a2961c5 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowHeader.cs @@ -0,0 +1,443 @@ +// 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.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.Utilities; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Represents an individual row header. + /// + [TemplatePart(Name = DATAGRIDROWHEADER_elementRootName, Type = typeof(FrameworkElement))] + + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateNormal, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateNormalCurrentRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateNormalEditingRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateNormalEditingRowFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOver, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverCurrentRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverEditingRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverEditingRowFocused, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverSelected, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverSelectedFocused, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverSelectedCurrentRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateSelected, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateSelectedCurrentRow, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateSelectedCurrentRowFocused, GroupName = VisualStates.GroupCommon)] + [TemplateVisualState(Name = DATAGRIDROWHEADER_stateSelectedFocused, GroupName = VisualStates.GroupCommon)] + + [TemplateVisualState(Name = VisualStates.StateRowInvalid, GroupName = VisualStates.GroupValidation)] + [TemplateVisualState(Name = VisualStates.StateRowValid, GroupName = VisualStates.GroupValidation)] + public partial class DataGridRowHeader : ContentControl + { + private const string DATAGRIDROWHEADER_elementRootName = "RowHeaderRoot"; + private const double DATAGRIDROWHEADER_separatorThickness = 1; + + private const string DATAGRIDROWHEADER_statePointerOver = "PointerOver"; + private const string DATAGRIDROWHEADER_statePointerOverCurrentRow = "PointerOverCurrentRow"; + private const string DATAGRIDROWHEADER_statePointerOverEditingRow = "PointerOverUnfocusedEditingRow"; + private const string DATAGRIDROWHEADER_statePointerOverEditingRowFocused = "PointerOverEditingRow"; + private const string DATAGRIDROWHEADER_statePointerOverSelected = "PointerOverUnfocusedSelected"; + private const string DATAGRIDROWHEADER_statePointerOverSelectedCurrentRow = "PointerOverUnfocusedCurrentRowSelected"; + private const string DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocused = "PointerOverCurrentRowSelected"; + private const string DATAGRIDROWHEADER_statePointerOverSelectedFocused = "PointerOverSelected"; + private const string DATAGRIDROWHEADER_stateNormal = "Normal"; + private const string DATAGRIDROWHEADER_stateNormalCurrentRow = "NormalCurrentRow"; + private const string DATAGRIDROWHEADER_stateNormalEditingRow = "UnfocusedEditingRow"; + private const string DATAGRIDROWHEADER_stateNormalEditingRowFocused = "NormalEditingRow"; + private const string DATAGRIDROWHEADER_stateSelected = "UnfocusedSelected"; + private const string DATAGRIDROWHEADER_stateSelectedCurrentRow = "UnfocusedCurrentRowSelected"; + private const string DATAGRIDROWHEADER_stateSelectedCurrentRowFocused = "NormalCurrentRowSelected"; + private const string DATAGRIDROWHEADER_stateSelectedFocused = "NormalSelected"; + + private const byte DATAGRIDROWHEADER_statePointerOverCode = 0; + private const byte DATAGRIDROWHEADER_statePointerOverCurrentRowCode = 1; + private const byte DATAGRIDROWHEADER_statePointerOverEditingRowCode = 2; + private const byte DATAGRIDROWHEADER_statePointerOverEditingRowFocusedCode = 3; + private const byte DATAGRIDROWHEADER_statePointerOverSelectedCode = 4; + private const byte DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowCode = 5; + private const byte DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocusedCode = 6; + private const byte DATAGRIDROWHEADER_statePointerOverSelectedFocusedCode = 7; + private const byte DATAGRIDROWHEADER_stateNormalCode = 8; + private const byte DATAGRIDROWHEADER_stateNormalCurrentRowCode = 9; + private const byte DATAGRIDROWHEADER_stateNormalEditingRowCode = 10; + private const byte DATAGRIDROWHEADER_stateNormalEditingRowFocusedCode = 11; + private const byte DATAGRIDROWHEADER_stateSelectedCode = 12; + private const byte DATAGRIDROWHEADER_stateSelectedCurrentRowCode = 13; + private const byte DATAGRIDROWHEADER_stateSelectedCurrentRowFocusedCode = 14; + private const byte DATAGRIDROWHEADER_stateSelectedFocusedCode = 15; + private const byte DATAGRIDROWHEADER_stateNullCode = 255; + + private static byte[] _fallbackStateMapping = new byte[] + { + DATAGRIDROWHEADER_stateNormalCode, + DATAGRIDROWHEADER_stateNormalCurrentRowCode, + DATAGRIDROWHEADER_statePointerOverEditingRowFocusedCode, + DATAGRIDROWHEADER_stateNormalEditingRowFocusedCode, + DATAGRIDROWHEADER_statePointerOverSelectedFocusedCode, + DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocusedCode, + DATAGRIDROWHEADER_stateSelectedFocusedCode, + DATAGRIDROWHEADER_stateSelectedFocusedCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNormalCode, + DATAGRIDROWHEADER_stateNormalEditingRowFocusedCode, + DATAGRIDROWHEADER_stateSelectedCurrentRowFocusedCode, + DATAGRIDROWHEADER_stateSelectedFocusedCode, + DATAGRIDROWHEADER_stateSelectedCurrentRowFocusedCode, + DATAGRIDROWHEADER_stateNormalCurrentRowCode, + DATAGRIDROWHEADER_stateNormalCode, + }; + + private static byte[] _idealStateMapping = new byte[] + { + DATAGRIDROWHEADER_stateNormalCode, + DATAGRIDROWHEADER_stateNormalCode, + DATAGRIDROWHEADER_statePointerOverCode, + DATAGRIDROWHEADER_statePointerOverCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateSelectedCode, + DATAGRIDROWHEADER_stateSelectedFocusedCode, + DATAGRIDROWHEADER_statePointerOverSelectedCode, + DATAGRIDROWHEADER_statePointerOverSelectedFocusedCode, + DATAGRIDROWHEADER_stateNormalEditingRowCode, + DATAGRIDROWHEADER_stateNormalEditingRowFocusedCode, + DATAGRIDROWHEADER_statePointerOverEditingRowCode, + DATAGRIDROWHEADER_statePointerOverEditingRowFocusedCode, + DATAGRIDROWHEADER_stateNormalCurrentRowCode, + DATAGRIDROWHEADER_stateNormalCurrentRowCode, + DATAGRIDROWHEADER_statePointerOverCurrentRowCode, + DATAGRIDROWHEADER_statePointerOverCurrentRowCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateNullCode, + DATAGRIDROWHEADER_stateSelectedCurrentRowCode, + DATAGRIDROWHEADER_stateSelectedCurrentRowFocusedCode, + DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowCode, + DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocusedCode, + DATAGRIDROWHEADER_stateNormalEditingRowCode, + DATAGRIDROWHEADER_stateNormalEditingRowFocusedCode, + DATAGRIDROWHEADER_statePointerOverEditingRowCode, + DATAGRIDROWHEADER_statePointerOverEditingRowFocusedCode + }; + + private static string[] _stateNames = new string[] + { + DATAGRIDROWHEADER_statePointerOver, + DATAGRIDROWHEADER_statePointerOverCurrentRow, + DATAGRIDROWHEADER_statePointerOverEditingRow, + DATAGRIDROWHEADER_statePointerOverEditingRowFocused, + DATAGRIDROWHEADER_statePointerOverSelected, + DATAGRIDROWHEADER_statePointerOverSelectedCurrentRow, + DATAGRIDROWHEADER_statePointerOverSelectedCurrentRowFocused, + DATAGRIDROWHEADER_statePointerOverSelectedFocused, + DATAGRIDROWHEADER_stateNormal, + DATAGRIDROWHEADER_stateNormalCurrentRow, + DATAGRIDROWHEADER_stateNormalEditingRow, + DATAGRIDROWHEADER_stateNormalEditingRowFocused, + DATAGRIDROWHEADER_stateSelected, + DATAGRIDROWHEADER_stateSelectedCurrentRow, + DATAGRIDROWHEADER_stateSelectedCurrentRowFocused, + DATAGRIDROWHEADER_stateSelectedFocused + }; + + private FrameworkElement _rootElement; + + /// + /// Initializes a new instance of the class. + /// + public DataGridRowHeader() + { + this.IsTapEnabled = true; + + this.AddHandler(UIElement.TappedEvent, new TappedEventHandler(DataGridRowHeader_Tapped), true /*handledEventsToo*/); + + DefaultStyleKey = typeof(DataGridRowHeader); + } + + /// + /// Gets or sets the used to paint the row header separator lines. + /// + public Brush SeparatorBrush + { + get { return GetValue(SeparatorBrushProperty) as Brush; } + set { SetValue(SeparatorBrushProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SeparatorBrushProperty = + DependencyProperty.Register( + "SeparatorBrush", + typeof(Brush), + typeof(DataGridRowHeader), + null); + + /// + /// Gets or sets a value indicating whether the row header separator lines are visible. + /// + public Visibility SeparatorVisibility + { + get { return (Visibility)GetValue(SeparatorVisibilityProperty); } + set { SetValue(SeparatorVisibilityProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SeparatorVisibilityProperty = + DependencyProperty.Register( + "SeparatorVisibility", + typeof(Visibility), + typeof(DataGridRowHeader), + new PropertyMetadata(Visibility.Visible)); + + private DataGrid OwningGrid + { + get + { + if (this.OwningRow != null) + { + return this.OwningRow.OwningGrid; + } + else if (this.OwningRowGroupHeader != null) + { + return this.OwningRowGroupHeader.OwningGrid; + } + + return null; + } + } + + private DataGridRow OwningRow + { + get + { + return this.Owner as DataGridRow; + } + } + + private DataGridRowGroupHeader OwningRowGroupHeader + { + get + { + return this.Owner as DataGridRowGroupHeader; + } + } + + internal Control Owner + { + get; + set; + } + + private int Slot + { + get + { + if (this.OwningRow != null) + { + return this.OwningRow.Slot; + } + else if (this.OwningRowGroupHeader != null) + { + return this.OwningRowGroupHeader.RowGroupInfo.Slot; + } + + return -1; + } + } + + /// + /// Builds the visual tree for the row header when a new template is applied. + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _rootElement = GetTemplateChild(DATAGRIDROWHEADER_elementRootName) as FrameworkElement; + if (_rootElement != null) + { + ApplyOwnerState(false /*animate*/); + } + } + + /// + /// Measures the children of a to prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (this.OwningRow == null || this.OwningGrid == null) + { + return base.MeasureOverride(availableSize); + } + + double measureHeight = double.IsNaN(this.OwningGrid.RowHeight) ? availableSize.Height : this.OwningGrid.RowHeight; + double measureWidth = double.IsNaN(this.OwningGrid.RowHeaderWidth) ? availableSize.Width : this.OwningGrid.RowHeaderWidth; + Size measuredSize = base.MeasureOverride(new Size(measureWidth, measureHeight)); + + // Auto grow the row header or force it to a fixed width based on the DataGrid's setting + if (!double.IsNaN(this.OwningGrid.RowHeaderWidth) || measuredSize.Width < this.OwningGrid.ActualRowHeaderWidth) + { + return new Size(this.OwningGrid.ActualRowHeaderWidth, measuredSize.Height); + } + + return measuredSize; + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridRowHeaderAutomationPeer(this); + } + + internal void ApplyOwnerState(bool animate) + { + if (_rootElement != null && this.Owner != null && this.Owner.Visibility == Visibility.Visible) + { + byte idealStateMappingIndex = 0; + + if (this.OwningRow != null) + { + if (this.OwningRow.IsValid) + { + VisualStates.GoToState(this, true, VisualStates.StateRowValid); + } + else + { + VisualStates.GoToState(this, true, VisualStates.StateRowInvalid, VisualStates.StateRowValid); + } + + if (this.OwningGrid != null) + { + if (this.OwningGrid.CurrentSlot == this.OwningRow.Slot) + { + idealStateMappingIndex += 16; + } + + if (this.OwningGrid.ContainsFocus) + { + idealStateMappingIndex += 1; + } + } + + if (this.OwningRow.IsSelected || this.OwningRow.IsEditing) + { + idealStateMappingIndex += 8; + } + + if (this.OwningRow.IsEditing) + { + idealStateMappingIndex += 4; + } + + if (this.OwningRow.IsPointerOver) + { + idealStateMappingIndex += 2; + } + } + else if (this.OwningRowGroupHeader != null && this.OwningGrid != null && this.OwningGrid.CurrentSlot == this.OwningRowGroupHeader.RowGroupInfo.Slot) + { + idealStateMappingIndex += 16; + } + + byte stateCode = _idealStateMapping[idealStateMappingIndex]; + Debug.Assert(stateCode != DATAGRIDROWHEADER_stateNullCode, "Expected stateCode other than DATAGRIDROWHEADER_stateNullCode."); + + string storyboardName; + while (stateCode != DATAGRIDROWHEADER_stateNullCode) + { + storyboardName = _stateNames[stateCode]; + if (VisualStateManager.GoToState(this, storyboardName, animate)) + { + break; + } + else + { + // The state wasn't implemented so fall back to the next one + stateCode = _fallbackStateMapping[stateCode]; + } + } + } + } + + /// + /// Ensures that the correct Style is applied to this object. + /// + /// Caller's previous associated Style + internal void EnsureStyle(Style previousStyle) + { + if (this.Style != null && + this.OwningRow != null && + this.Style != this.OwningRow.HeaderStyle && + this.OwningRowGroupHeader != null && + this.Style != this.OwningRowGroupHeader.HeaderStyle && + this.OwningGrid != null && + this.Style != this.OwningGrid.RowHeaderStyle && + this.Style != previousStyle) + { + return; + } + + Style style = null; + if (this.OwningRow != null) + { + style = this.OwningRow.HeaderStyle; + } + + if (style == null && this.OwningGrid != null) + { + style = this.OwningGrid.RowHeaderStyle; + } + + this.SetStyleWithType(style); + } + + private void DataGridRowHeader_Tapped(object sender, TappedRoutedEventArgs e) + { + if (this.OwningGrid != null && !this.OwningGrid.HasColumnUserInteraction) + { + if (!e.Handled && this.OwningGrid.IsTabStop) + { + bool success = this.OwningGrid.Focus(FocusState.Programmatic); + Debug.Assert(success, "Expected successful focus change."); + } + + if (this.OwningRow != null) + { + Debug.Assert(sender is DataGridRowHeader, "Expected sender is DataGridRowHeader."); + Debug.Assert(sender as ContentControl == this, "Expected sender is this."); + + e.Handled = this.OwningGrid.UpdateStateOnTapped(e, -1, this.Slot, false /*allowEdit*/); + this.OwningGrid.UpdatedStateOnTapped = true; + } + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRows.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRows.cs new file mode 100644 index 0000000..e71be5b --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRows.cs @@ -0,0 +1,3608 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.UI.Utilities; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Control to represent data in columns and rows. + /// + public partial class DataGrid + { + internal bool AreRowBottomGridLinesRequired + { + get + { + return (this.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || this.GridLinesVisibility == DataGridGridLinesVisibility.All) && this.HorizontalGridLinesBrush != null; + } + } + + internal int FirstVisibleSlot + { + get + { + return (this.SlotCount > 0) ? GetNextVisibleSlot(-1) : -1; + } + } + + internal int FrozenColumnCountWithFiller + { + get + { + int count = this.FrozenColumnCount; + if (this.ColumnsInternal.RowGroupSpacerColumn.IsRepresented && (this.AreRowGroupHeadersFrozen || count > 0)) + { + // Either the RowGroupHeaders are frozen by default or the user set a frozen column count. In both cases, we need to freeze + // one more column than the what the public value says + count++; + } + + return count; + } + } + + internal int LastVisibleSlot + { + get + { + return (this.SlotCount > 0) ? this.GetPreviousVisibleSlot(this.SlotCount) : -1; + } + } + + // Cumulated height of all known rows, including the gridlines and details section. + // This property returns an approximation of the actual total row heights and also + // updates the RowHeightEstimate + private double EdgedRowsHeightCalculated + { + get + { + // If we're not displaying any rows or if we have infinite space the, relative height of our rows is 0 + if (this.DisplayData.LastScrollingSlot == -1 || double.IsPositiveInfinity(this.AvailableSlotElementRoom)) + { + if (_oldEdgedRowsHeightCalculated > 0) + { + _oldEdgedRowsHeightCalculated = 0; + + LoadMoreDataFromIncrementalItemsSource(0); + } + + return 0; + } + + Debug.Assert(this.DisplayData.LastScrollingSlot >= 0, "Expected positive DisplayData.LastScrollingSlot."); + Debug.Assert(_verticalOffset >= 0, "Expected positive _verticalOffset."); + Debug.Assert(this.NegVerticalOffset >= 0, "Expected positive NegVerticalOffset."); + + // Height of all rows above the viewport + double totalRowsHeight = _verticalOffset - this.NegVerticalOffset; + + // Add the height of all the rows currently displayed, AvailableRowRoom + // is not always up to date enough for this + foreach (UIElement element in this.DisplayData.GetScrollingElements()) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + totalRowsHeight += row.TargetHeight; + } + else + { + totalRowsHeight += element.EnsureMeasured().DesiredSize.Height; + } + } + + // Details up to and including viewport + int detailsCount = GetDetailsCountInclusive(0, this.DisplayData.LastScrollingSlot); + + // Subtract details that were accounted for from the totalRowsHeight + totalRowsHeight -= detailsCount * this.RowDetailsHeightEstimate; + + // Update the RowHeightEstimate if we have more row information + if (this.DisplayData.LastScrollingSlot >= _lastEstimatedRow) + { + _lastEstimatedRow = this.DisplayData.LastScrollingSlot; + this.RowHeightEstimate = totalRowsHeight / (_lastEstimatedRow + 1 - _collapsedSlotsTable.GetIndexCount(0, _lastEstimatedRow)); + } + + // Calculate estimates for what's beyond the viewport + if (this.VisibleSlotCount > this.DisplayData.NumDisplayedScrollingElements) + { + int remainingRowCount = this.SlotCount - this.DisplayData.LastScrollingSlot - _collapsedSlotsTable.GetIndexCount(this.DisplayData.LastScrollingSlot, this.SlotCount - 1) - 1; + + // Add estimation for the cell heights of all rows beyond our viewport + totalRowsHeight += this.RowHeightEstimate * remainingRowCount; + + // Add the rest of the details beyond the viewport + detailsCount += GetDetailsCountInclusive(this.DisplayData.LastScrollingSlot + 1, this.SlotCount - 1); + } + + // TODO: Update the DetailsHeightEstimate + double totalDetailsHeight = detailsCount * this.RowDetailsHeightEstimate; + double newEdgedRowsHeightCalculated = totalRowsHeight + totalDetailsHeight; + bool loadMoreDataFromIncrementalItemsSource = newEdgedRowsHeightCalculated < _oldEdgedRowsHeightCalculated; + + _oldEdgedRowsHeightCalculated = newEdgedRowsHeightCalculated; + + if (loadMoreDataFromIncrementalItemsSource) + { + LoadMoreDataFromIncrementalItemsSource(newEdgedRowsHeightCalculated); + } + + return newEdgedRowsHeightCalculated; + } + } + + /// + /// Collapses the DataGridRowGroupHeader that represents a given CollectionViewGroup + /// + /// CollectionViewGroup + /// Set to true to collapse all Subgroups + public void CollapseRowGroup(ICollectionViewGroup collectionViewGroup, bool collapseAllSubgroups) + { + if (this.WaitForLostFocus(() => { this.CollapseRowGroup(collectionViewGroup, collapseAllSubgroups); }) || + collectionViewGroup == null || !this.CommitEdit()) + { + return; + } + + EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), Visibility.Collapsed, true); + + if (collapseAllSubgroups) + { + foreach (object groupObj in collectionViewGroup.GroupItems) + { + ICollectionViewGroup subGroup = groupObj as ICollectionViewGroup; + if (subGroup != null) + { + CollapseRowGroup(subGroup, collapseAllSubgroups); + } + } + } + } + + /// + /// Expands the DataGridRowGroupHeader that represents a given CollectionViewGroup + /// + /// CollectionViewGroup + /// Set to true to expand all Subgroups + public void ExpandRowGroup(ICollectionViewGroup collectionViewGroup, bool expandAllSubgroups) + { + if (this.WaitForLostFocus(() => { this.ExpandRowGroup(collectionViewGroup, expandAllSubgroups); }) || + collectionViewGroup == null || !this.CommitEdit()) + { + if (collectionViewGroup == null || !this.CommitEdit()) + { + return; + } + } + + EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), Visibility.Visible, true); + + if (expandAllSubgroups) + { + foreach (object groupObj in collectionViewGroup.GroupItems) + { + ICollectionViewGroup subGroup = groupObj as ICollectionViewGroup; + if (subGroup != null) + { + ExpandRowGroup(subGroup, expandAllSubgroups); + } + } + } + } + + /// + /// Raises the event. + /// + /// The event data. + protected internal virtual void OnRowDetailsVisibilityChanged(DataGridRowDetailsEventArgs e) + { + this.RowDetailsVisibilityChanged?.Invoke(this, e); + } + + /// + /// Clears the entire selection. Displayed rows are deselected explicitly to visualize + /// potential transition effects + /// + internal void ClearRowSelection(bool resetAnchorSlot) + { + if (resetAnchorSlot) + { + this.AnchorSlot = -1; + } + + if (_selectedItems.Count > 0) + { + _noSelectionChangeCount++; + try + { + // Individually deselecting displayed rows to view potential transitions + for (int slot = this.DisplayData.FirstScrollingSlot; + slot > -1 && slot <= this.DisplayData.LastScrollingSlot; + slot++) + { + DataGridRow row = this.DisplayData.GetDisplayedElement(slot) as DataGridRow; + if (row != null) + { + if (_selectedItems.ContainsSlot(row.Slot)) + { + SelectSlot(row.Slot, false); + } + } + } + + _selectedItems.ClearRows(); + this.SelectionHasChanged = true; + } + finally + { + this.NoSelectionChangeCount--; + } + } + } + + /// + /// Clears the entire selection except the indicated row. Displayed rows are deselected explicitly to + /// visualize potential transition effects. The row indicated is selected if it is not already. + /// + internal void ClearRowSelection(int slotException, bool setAnchorSlot) + { + _noSelectionChangeCount++; + try + { + bool exceptionAlreadySelected = false; + if (_selectedItems.Count > 0) + { + // Individually deselecting displayed rows to view potential transitions + for (int slot = this.DisplayData.FirstScrollingSlot; + slot > -1 && slot <= this.DisplayData.LastScrollingSlot; + slot++) + { + if (slot != slotException && _selectedItems.ContainsSlot(slot)) + { + SelectSlot(slot, false); + this.SelectionHasChanged = true; + } + } + + exceptionAlreadySelected = _selectedItems.ContainsSlot(slotException); + int selectedCount = _selectedItems.Count; + if (selectedCount > 0) + { + if (selectedCount > 1) + { + this.SelectionHasChanged = true; + } + else + { + int currentlySelectedSlot = _selectedItems.GetIndexes().First(); + if (currentlySelectedSlot != slotException) + { + this.SelectionHasChanged = true; + } + } + + _selectedItems.ClearRows(); + } + } + + if (exceptionAlreadySelected) + { + // Exception row was already selected. It just needs to be marked as selected again. + // No transition involved. + _selectedItems.SelectSlot(slotException, true /*select*/); + if (setAnchorSlot) + { + this.AnchorSlot = slotException; + } + } + else + { + // Exception row was not selected. It needs to be selected with potential transition + SetRowSelection(slotException, true /*isSelected*/, setAnchorSlot); + } + } + finally + { + this.NoSelectionChangeCount--; + } + } + + internal int GetCollapsedSlotCount(int startSlot, int endSlot) + { + return _collapsedSlotsTable.GetIndexCount(startSlot, endSlot); + } + + internal int GetNextVisibleSlot(int slot) + { + return _collapsedSlotsTable.GetNextGap(slot); + } + + internal int GetPreviousVisibleSlot(int slot) + { + return _collapsedSlotsTable.GetPreviousGap(slot); + } + + internal Visibility GetRowDetailsVisibility(int rowIndex) + { + return GetRowDetailsVisibility(rowIndex, this.RowDetailsVisibilityMode); + } + + internal Visibility GetRowDetailsVisibility(int rowIndex, DataGridRowDetailsVisibilityMode gridLevelRowDetailsVisibility) + { + Debug.Assert(rowIndex != -1, "Expected rowIndex other than -1."); + if (_showDetailsTable.Contains(rowIndex)) + { + // The user explicitly set DetailsVisibility on a row so we should respect that + return _showDetailsTable.GetValueAt(rowIndex); + } + else + { + if (gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.Visible || + (gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.VisibleWhenSelected && + _selectedItems.ContainsSlot(SlotFromRowIndex(rowIndex)))) + { + return Visibility.Visible; + } + else + { + return Visibility.Collapsed; + } + } + } + + /// + /// Returns the row associated to the provided backend data item. + /// + /// backend data item + /// null if the DataSource is null, the provided item in not in the source, or the item is not displayed; otherwise, the associated Row + internal DataGridRow GetRowFromItem(object dataItem) + { + int rowIndex = this.DataConnection.IndexOf(dataItem); + if (rowIndex < 0) + { + return null; + } + + int slot = SlotFromRowIndex(rowIndex); + return IsSlotVisible(slot) ? this.DisplayData.GetDisplayedElement(slot) as DataGridRow : null; + } + + internal bool GetRowSelection(int slot) + { + Debug.Assert(slot != -1, "Expected slot other than -1."); + return _selectedItems.ContainsSlot(slot); + } + + internal void InsertElementAt( + int slot, + int rowIndex, + object item, + DataGridRowGroupInfo groupInfo, + bool isCollapsed) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(slot <= this.SlotCount, "Expected slot smaller than or equal to SlotCount."); + + bool isRow = rowIndex != -1; + if (isCollapsed || (this.IsReadOnly && rowIndex == this.DataConnection.NewItemPlaceholderIndex)) + { + InsertElement(slot, null /*element*/, true /*updateVerticalScrollBarOnly*/, true /*isCollapsed*/, isRow); + } + else if (SlotIsDisplayed(slot)) + { + // Row at that index needs to be displayed + if (isRow) + { + InsertElement(slot, GenerateRow(rowIndex, slot, item), false /*updateVerticalScrollBarOnly*/, false /*isCollapsed*/, isRow); + } + else + { + InsertElement(slot, GenerateRowGroupHeader(slot, groupInfo), false /*updateVerticalScrollBarOnly*/, false /*isCollapsed*/, isRow); + } + } + else + { + InsertElement(slot, null, _vScrollBar == null || _vScrollBar.Visibility == Visibility.Visible /*updateVerticalScrollBarOnly*/, false /*isCollapsed*/, isRow); + } + } + + internal void InsertRowAt(int rowIndex) + { + int slot = SlotFromRowIndex(rowIndex); + object item = this.DataConnection.GetDataItem(rowIndex); + + // isCollapsed below is always false because we only use the method if we're not grouping + InsertElementAt( + slot, + rowIndex, + item, + null /*DataGridRowGroupInfo*/, + false /*isCollapsed*/); + } + + internal bool IsColumnDisplayed(int columnIndex) + { + return columnIndex >= this.FirstDisplayedNonFillerColumnIndex && columnIndex <= this.DisplayData.LastTotallyDisplayedScrollingCol; + } + + internal bool IsRowRecyclable(DataGridRow row) + { + return row != this.EditingRow && row != _focusedRow; + } + + internal bool IsSlotVisible(int slot) + { + return slot >= this.DisplayData.FirstScrollingSlot && + slot <= this.DisplayData.LastScrollingSlot && + slot != -1 && + !_collapsedSlotsTable.Contains(slot); + } + + // detailsElement is the FrameworkElement created by the DetailsTemplate + internal void OnUnloadingRowDetails(DataGridRow row, FrameworkElement detailsElement) + { + OnUnloadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement)); + } + + // detailsElement is the FrameworkElement created by the DetailsTemplate + internal void OnLoadingRowDetails(DataGridRow row, FrameworkElement detailsElement) + { + OnLoadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement)); + } + + internal void OnRowDetailsVisibilityPropertyChanged(int rowIndex, Visibility visibility) + { + Debug.Assert(rowIndex >= 0, "Expected positive rowIndex."); + Debug.Assert(rowIndex < this.SlotCount, "Expected rowIndex smaller than SlotCount."); + + _showDetailsTable.AddValue(rowIndex, visibility); + } + + internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, Visibility newVisibility, bool setCurrent) + { + Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.GroupItems.Count > 0, "Expected positive groupHeader.RowGroupInfo.CollectionViewGroup.GroupItems.Count."); + + if (this.WaitForLostFocus(() => { this.OnRowGroupHeaderToggled(groupHeader, newVisibility, setCurrent); }) || !this.CommitEdit()) + { + return; + } + + if (setCurrent && this.CurrentSlot != groupHeader.RowGroupInfo.Slot) + { + // Most of the time this is set by the MouseLeftButtonDown handler but validation could cause that code path to fail + UpdateSelectionAndCurrency(this.CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, false /*scrollIntoView*/); + } + + UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newVisibility, true /*isHeaderDisplayed*/); + + ComputeScrollBarsLayout(); + + // We need force arrange since our Scrollings Rows could update without automatically triggering layout + InvalidateRowsArrange(); + } + + internal void OnRowsMeasure() + { + if (!DoubleUtil.IsZero(this.DisplayData.PendingVerticalScrollHeight)) + { + ScrollSlotsByHeight(this.DisplayData.PendingVerticalScrollHeight); + this.DisplayData.PendingVerticalScrollHeight = 0; + } + } + + internal void OnSublevelIndentUpdated(DataGridRowGroupHeader groupHeader, double newValue) + { + Debug.Assert(this.DataConnection.CollectionView != null, "Expected non-null DataConnection.CollectionView."); + Debug.Assert(this.DataConnection.CollectionView.CollectionGroups != null, "Expected non-null DataConnection.CollectionView.CollectionGroups."); + Debug.Assert(this.RowGroupSublevelIndents != null, "Expected non-null RowGroupSublevelIndents."); + +#if FEATURE_ICOLLECTIONVIEW_GROUP + int groupLevelCount = this.DataConnection.CollectionView.GroupDescriptions.Count; +#else + int groupLevelCount = 1; +#endif + Debug.Assert(groupHeader.Level >= 0, "Expected positive groupHeader.Level."); + Debug.Assert(groupHeader.Level < groupLevelCount, "Expected groupHeader.Level smaller than groupLevelCount."); + + double oldValue = this.RowGroupSublevelIndents[groupHeader.Level]; + if (groupHeader.Level > 0) + { + oldValue -= this.RowGroupSublevelIndents[groupHeader.Level - 1]; + } + + // Update the affected values in our table by the amount affected + double change = newValue - oldValue; + for (int i = groupHeader.Level; i < groupLevelCount; i++) + { + this.RowGroupSublevelIndents[i] += change; + Debug.Assert(this.RowGroupSublevelIndents[i] >= 0, "Expected positive RowGroupSublevelIndents[i]."); + } + + EnsureRowGroupSpacerColumnWidth(groupLevelCount); + } + + internal void RefreshRows(bool recycleRows, bool clearRows) + { + if (_measured) + { + // _desiredCurrentColumnIndex is used in MakeFirstDisplayedCellCurrentCell to set the + // column position back to what it was before the refresh + _desiredCurrentColumnIndex = this.CurrentColumnIndex; + double verticalOffset = _verticalOffset; + if (this.DisplayData.PendingVerticalScrollHeight > 0) + { + // Use the pending vertical scrollbar position if there is one, in the case that the collection + // has been reset multiple times in a row. + verticalOffset = this.DisplayData.PendingVerticalScrollHeight; + } + + VerticalOffset = 0; + this.NegVerticalOffset = 0; + + if (clearRows) + { + ClearRows(recycleRows); + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + RefreshSlotCounts(); + } + + RefreshRowGroupHeaders(); + + // Update the CurrentSlot because it might have changed + if (recycleRows && this.DataConnection.CollectionView != null) + { + this.CurrentSlot = this.DataConnection.CollectionView.CurrentPosition == -1 + ? -1 : SlotFromRowIndex(this.DataConnection.CollectionView.CurrentPosition); + if (this.CurrentSlot == -1) + { + SetCurrentCellCore(-1, -1); + } + } + + if (this.DataConnection != null && this.ColumnsItemsInternal.Count > 0) + { + int slotCount = this.DataConnection.Count; + slotCount += this.RowGroupHeadersTable.IndexCount; + AddSlots(slotCount); + InvalidateMeasure(); + } + + EnsureRowGroupSpacerColumn(); + + if (this.VerticalScrollBar != null) + { + this.DisplayData.PendingVerticalScrollHeight = Math.Min(verticalOffset, this.VerticalScrollBar.Maximum); + } + } + else + { + if (clearRows) + { + ClearRows(recycleRows /*recycle*/); + } + + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + RefreshSlotCounts(); + } + } + + internal void RemoveRowAt(int rowIndex, object item) + { + RemoveElementAt(SlotFromRowIndex(rowIndex), item, true); + } + + internal DataGridRowGroupInfo RowGroupInfoFromCollectionViewGroup(ICollectionViewGroup collectionViewGroup) + { + foreach (int slot in this.RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo.CollectionViewGroup == collectionViewGroup) + { + return rowGroupInfo; + } + } + + return null; + } + + internal int RowIndexFromSlot(int slot) + { + return slot - this.RowGroupHeadersTable.GetIndexCount(0, slot); + } + + internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally) + { + Debug.Assert(_collapsedSlotsTable.Contains(slot) || !IsSlotOutOfBounds(slot), "Expected _collapsedSlotsTable.Contains(slot) is true or IsSlotOutOfBounds(slot) is false."); + + if (scrolledHorizontally && this.DisplayData.FirstScrollingSlot <= slot && this.DisplayData.LastScrollingSlot >= slot) + { + // If the slot is displayed and we scrolled horizontally, column virtualization could cause the rows to grow. + // As a result we need to force measure on the rows we're displaying and recalculate our First and Last slots + // so they're accurate + foreach (DataGridRow row in this.DisplayData.GetScrollingElements(true /*onlyRows*/)) + { + row.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + UpdateDisplayedRows(this.DisplayData.FirstScrollingSlot, this.CellsHeight); + } + + if (this.DisplayData.FirstScrollingSlot < slot && this.DisplayData.LastScrollingSlot > slot) + { + // The row is already displayed in its entirety + return true; + } + else if (this.DisplayData.FirstScrollingSlot == slot && slot != -1) + { + if (!DoubleUtil.IsZero(this.NegVerticalOffset)) + { + // First displayed row is partially scrolled of. Let's scroll it so that this.NegVerticalOffset becomes 0. + this.DisplayData.PendingVerticalScrollHeight = -this.NegVerticalOffset; + InvalidateRowsMeasure(false /*invalidateIndividualRows*/); + } + + return true; + } + + double deltaY = 0; + int firstFullSlot; + if (this.DisplayData.FirstScrollingSlot > slot) + { + // Scroll up to the new row so it becomes the first displayed row + firstFullSlot = this.DisplayData.FirstScrollingSlot - 1; + if (DoubleUtil.GreaterThan(this.NegVerticalOffset, 0)) + { + deltaY = -this.NegVerticalOffset; + } + + deltaY -= GetSlotElementsHeight(slot, firstFullSlot); + if (this.DisplayData.FirstScrollingSlot - slot > 1) + { + // TODO: This will likely discard and create a small number of the same rows so we could probably + // optimize this. The optimization would only affect the PageUp key. + ResetDisplayedRows(); + } + + this.NegVerticalOffset = 0; + UpdateDisplayedRows(slot, this.CellsHeight); + } + else if (this.DisplayData.LastScrollingSlot <= slot) + { + // Scroll down to the new row so it's entirely displayed. If the height of the row + // is greater than the height of the DataGrid, then show the top of the row at the top + // of the grid. + firstFullSlot = this.DisplayData.LastScrollingSlot; + + // Figure out how much of the last row is cut off. + double rowHeight = GetExactSlotElementHeight(this.DisplayData.LastScrollingSlot); + double availableHeight = this.AvailableSlotElementRoom + rowHeight; + if (DoubleUtil.AreClose(rowHeight, availableHeight)) + { + if (this.DisplayData.LastScrollingSlot == slot) + { + // We're already at the very bottom so we don't need to scroll down further. + return true; + } + else + { + // We're already showing the entire last row so don't count it as part of the delta. + firstFullSlot++; + } + } + else if (rowHeight > availableHeight) + { + firstFullSlot++; + deltaY += rowHeight - availableHeight; + } + + // sum up the height of the rest of the full rows. + if (slot >= firstFullSlot) + { + deltaY += GetSlotElementsHeight(firstFullSlot, slot); + } + + // If the first row we're displaying is no longer adjacent to the rows we have + // simply discard the ones we have. + if (slot - this.DisplayData.LastScrollingSlot > 1) + { + ResetDisplayedRows(); + } + + if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), this.CellsHeight)) + { + // The entire row won't fit in the DataGrid so we start showing it from the top. + this.NegVerticalOffset = 0; + UpdateDisplayedRows(slot, this.CellsHeight); + } + else + { + UpdateDisplayedRowsFromBottom(slot); + } + } + + VerticalOffset += deltaY; + if (_verticalOffset < 0 || this.DisplayData.FirstScrollingSlot == 0) + { + // We scrolled too far because a row's height was larger than its approximation. + VerticalOffset = this.NegVerticalOffset; + } + + // TODO: in certain cases (eg, variable row height), this may not be true + Debug.Assert(DoubleUtil.LessThanOrClose(this.NegVerticalOffset, _verticalOffset), "Expected NegVerticalOffset is less than or close to _verticalOffset."); + + SetVerticalOffset(_verticalOffset); + + InvalidateMeasure(); + InvalidateRowsMeasure(false /*invalidateIndividualRows*/); + + return true; + } + + internal void SetRowSelection(int slot, bool isSelected, bool setAnchorSlot) + { + Debug.Assert(isSelected || !setAnchorSlot, "Expected isSelected is true or setAnchorSlot is false."); + Debug.Assert(!IsSlotOutOfSelectionBounds(slot), "Expected IsSlotOutOfSelectionBounds(slot) is false."); + _noSelectionChangeCount++; + try + { + if (this.SelectionMode == DataGridSelectionMode.Single && isSelected) + { + Debug.Assert(_selectedItems.Count <= 1, "Expected _selectedItems.Count smaller than or equal to 1."); + if (_selectedItems.Count > 0) + { + int currentlySelectedSlot = _selectedItems.GetIndexes().First(); + if (currentlySelectedSlot != slot) + { + SelectSlot(currentlySelectedSlot, false); + this.SelectionHasChanged = true; + } + } + } + + if (_selectedItems.ContainsSlot(slot) != isSelected) + { + SelectSlot(slot, isSelected); + this.SelectionHasChanged = true; + } + + if (setAnchorSlot) + { + this.AnchorSlot = slot; + } + } + finally + { + this.NoSelectionChangeCount--; + } + } + + // For now, all scenarios are for isSelected == true. + internal void SetRowsSelection(int startSlot, int endSlot, bool isSelected = true) + { + Debug.Assert(startSlot >= 0, "Expected startSlot is positive."); + Debug.Assert(startSlot < this.SlotCount, "Expected startSlot is smaller than SlotCount."); + Debug.Assert(endSlot >= 0, "Expected endSlot is positive."); + Debug.Assert(endSlot < this.SlotCount, "Expected endSlot is smaller than SlotCount."); + Debug.Assert(startSlot <= endSlot, "Expected startSlot is smaller than or equal to endSlot."); + + _noSelectionChangeCount++; + try + { + if (isSelected && !_selectedItems.ContainsAll(startSlot, endSlot)) + { + // At least one row gets selected + SelectSlots(startSlot, endSlot, true); + this.SelectionHasChanged = true; + } + } + finally + { + this.NoSelectionChangeCount--; + } + } + + internal int SlotFromRowIndex(int rowIndex) + { + return rowIndex + this.RowGroupHeadersTable.GetIndexCountBeforeGap(0, rowIndex); + } + + private static void CorrectRowAfterDeletion(DataGridRow row, bool rowDeleted) + { + row.Slot--; + if (rowDeleted) + { + row.Index--; + } + } + + private static void CorrectRowAfterInsertion(DataGridRow row, bool rowInserted) + { + row.Slot++; + if (rowInserted) + { + row.Index++; + } + } + + private void AddSlotElement(int slot, UIElement element) + { +#if DEBUG + DataGridRow row = element as DataGridRow; + if (row != null) + { + Debug.Assert(row.OwningGrid == this, "Expected row.OwningGrid equals this DataGrid."); + Debug.Assert(row.Cells.Count == this.ColumnsItemsInternal.Count, "Expected row.Cells.Count equals this.ColumnsItemsInternal.Count."); + + int columnIndex = 0; + foreach (DataGridCell dataGridCell in row.Cells) + { + Debug.Assert(dataGridCell.OwningRow == row, "Expected dataGridCell.OwningRow equals row."); + Debug.Assert(dataGridCell.OwningColumn == this.ColumnsItemsInternal[columnIndex], "Expected dataGridCell.OwningColumn equals this.ColumnsItemsInternal[columnIndex]."); + columnIndex++; + } + } +#endif + Debug.Assert(slot == this.SlotCount, "Expected slot equals this.SlotCount."); + + OnAddedElement_Phase1(slot, element); + this.SlotCount++; + this.VisibleSlotCount++; + OnAddedElement_Phase2(slot, false /*updateVerticalScrollBarOnly*/); + OnElementsChanged(true /*grew*/); + } + + private void AddSlots(int totalSlots) + { + this.SlotCount = 0; + this.VisibleSlotCount = 0; + IEnumerator groupSlots = null; + int nextGroupSlot = -1; + if (this.RowGroupHeadersTable.RangeCount > 0) + { + groupSlots = this.RowGroupHeadersTable.GetIndexes().GetEnumerator(); + if (groupSlots != null && groupSlots.MoveNext()) + { + nextGroupSlot = groupSlots.Current; + } + } + + int slot = 0; + int addedRows = 0; + while (slot < totalSlots && this.AvailableSlotElementRoom > 0) + { + if (slot == nextGroupSlot) + { + DataGridRowGroupInfo groupRowInfo = this.RowGroupHeadersTable.GetValueAt(slot); + AddSlotElement(slot, GenerateRowGroupHeader(slot, groupRowInfo)); + nextGroupSlot = groupSlots.MoveNext() ? groupSlots.Current : -1; + } + else + { + AddSlotElement(slot, GenerateRow(addedRows, slot)); + addedRows++; + } + + slot++; + } + + if (slot < totalSlots) + { + this.SlotCount += totalSlots - slot; + this.VisibleSlotCount += totalSlots - slot; + OnAddedElement_Phase2(0, _vScrollBar == null || _vScrollBar.Visibility == Visibility.Visible /*updateVerticalScrollBarOnly*/); + OnElementsChanged(true /*grew*/); + } + } + + private void ApplyDisplayedRowsState(int startSlot, int endSlot) + { + int firstSlot = Math.Max(this.DisplayData.FirstScrollingSlot, startSlot); + int lastSlot = Math.Min(this.DisplayData.LastScrollingSlot, endSlot); + + if (firstSlot >= 0) + { + Debug.Assert(lastSlot >= firstSlot, "lastSlot greater than or equal to firstSlot."); + int slot = GetNextVisibleSlot(firstSlot - 1); + while (slot <= lastSlot) + { + DataGridRow row = this.DisplayData.GetDisplayedElement(slot) as DataGridRow; + if (row != null) + { + row.ApplyState(true /*animate*/); + } + + slot = GetNextVisibleSlot(slot); + } + } + } + + private void ClearRowGroupHeadersTable() + { + // Detach existing handlers on CollectionViewGroup.Items.CollectionChanged + foreach (int slot in this.RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo groupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (groupInfo.CollectionViewGroup.GroupItems != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + UnhookCollectionChangedListenerFromGroup(groupInfo.CollectionViewGroup.GroupItems as INotifyCollectionChanged, false /*removeFromTable*/); +#else + UnhookVectorChangedListenerFromGroup(groupInfo.CollectionViewGroup.GroupItems, false /*removeFromTable*/); +#endif + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + WeakEventListener weakPropertyChangedListener; + INotifyPropertyChanged inpc = groupInfo.CollectionViewGroup as INotifyPropertyChanged; + if (inpc != null && _groupsPropertyChangedListenersTable.TryGetValue(inpc, out weakPropertyChangedListener)) + { + weakPropertyChangedListener.Detach(); + } +#endif + } + + if (_topLevelGroup != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + UnhookCollectionChangedListenerFromGroup(_topLevelGroup as INotifyCollectionChanged, false /*removeFromTable*/); +#else + UnhookVectorChangedListenerFromGroup(_topLevelGroup, false /*removeFromTable*/); +#endif + _topLevelGroup = null; + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + _groupsPropertyChangedListenersTable.Clear(); + _groupsCollectionChangedListenersTable.Clear(); +#endif + + this.RowGroupHeadersTable.Clear(); + _collapsedSlotsTable.Clear(); + + _rowGroupHeightsByLevel = null; + RowGroupSublevelIndents = null; + } + + private void ClearRows(bool recycle) + { + // Need to clean up recycled rows even if the RowCount is 0 + SetCurrentCellCore(-1, -1, false /*commitEdit*/, false /*endRowEdit*/); + ClearRowSelection(true /*resetAnchorSlot*/); + UnloadElements(recycle); + + this.ClearShowDetailsTable(); + this.SlotCount = 0; + this.NegVerticalOffset = 0; + SetVerticalOffset(0); + ComputeScrollBarsLayout(); + } + + private void ClearShowDetailsTable() + { + _showDetailsTable.Clear(); +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (this.DataConnection.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd) + { + _showDetailsTable.AddValue(this.DataConnection.NewItemPlaceholderIndex, Visibility.Collapsed); + } +#endif + } + + // Updates _collapsedSlotsTable and returns the number of pixels that were collapsed + private double CollapseSlotsInTable(int startSlot, int endSlot, ref int slotsExpanded, int lastDisplayedSlot, ref double heightChangeBelowLastDisplayedSlot) + { + int firstSlot = startSlot; + int lastSlot; + double totalHeightChange = 0; + + // Figure out which slots actually need to be expanded since some might already be collapsed + while (firstSlot <= endSlot) + { + firstSlot = _collapsedSlotsTable.GetNextGap(firstSlot - 1); + int nextCollapsedSlot = _collapsedSlotsTable.GetNextIndex(firstSlot) - 1; + lastSlot = nextCollapsedSlot == -2 ? endSlot : Math.Min(endSlot, nextCollapsedSlot); + + if (firstSlot <= lastSlot) + { + double heightChange = GetHeightEstimate(firstSlot, lastSlot); + totalHeightChange -= heightChange; + slotsExpanded -= lastSlot - firstSlot + 1; + + if (lastSlot > lastDisplayedSlot) + { + if (firstSlot > lastDisplayedSlot) + { + heightChangeBelowLastDisplayedSlot -= heightChange; + } + else + { + heightChangeBelowLastDisplayedSlot -= GetHeightEstimate(lastDisplayedSlot + 1, lastSlot); + } + } + + firstSlot = lastSlot + 1; + } + } + + // Update _collapsedSlotsTable in one bulk operation + _collapsedSlotsTable.AddValues(startSlot, endSlot - startSlot + 1, Visibility.Collapsed); + + return totalHeightChange; + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + private void CollectionViewGroup_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "ItemCount") + { + DataGridRowGroupInfo rowGroupInfo = RowGroupInfoFromCollectionViewGroup(sender as CollectionViewGroup); + if (rowGroupInfo != null && IsSlotVisible(rowGroupInfo.Slot)) + { + DataGridRowGroupHeader rowGroupHeader = this.DisplayData.GetDisplayedElement(rowGroupInfo.Slot) as DataGridRowGroupHeader; + if (rowGroupHeader != null) + { + rowGroupHeader.UpdateTitleElements(); + } + } + } + } +#endif + + private void CorrectEditingRow() + { + if (this.EditingRow != null) + { + this.EditingRow.Index = this.DataConnection.IndexOf(this.EditingRow.DataContext); + this.EditingRow.Slot = this.SlotFromRowIndex(this.EditingRow.Index); + if (this.EditingRow.Index > 0) + { + // The collection actually removes and re-inserts the edited item during a commit operation. + // We recycle the editing row in this case in order to avoid generating a new element, but we don't + // care about refreshing its background/foreground until the item is added back (i.e. Index > 0). + this.EditingRow.EnsureBackground(); + this.EditingRow.EnsureForeground(); + } + } + } + + // This method is necessary for incrementing the LastSubItemSlot property of the group ancestors + // because CorrectSlotsAfterInsertion only increments those that come after the specified group. + private void CorrectLastSubItemSlotsAfterInsertion(DataGridRowGroupInfo subGroupInfo) + { + int subGroupSlot; + int subGroupLevel; + while (subGroupInfo != null) + { + subGroupLevel = subGroupInfo.Level; + subGroupInfo.LastSubItemSlot++; + + while (subGroupInfo != null && subGroupInfo.Level >= subGroupLevel) + { + subGroupSlot = this.RowGroupHeadersTable.GetPreviousIndex(subGroupInfo.Slot); + subGroupInfo = this.RowGroupHeadersTable.GetValueAt(subGroupSlot); + } + } + } + + /// + /// Adjusts the index of all displayed, loaded and edited rows after a row was deleted. + /// Removes the deleted row from the list of loaded rows if present. + /// + private void CorrectSlotsAfterDeletion(int slotDeleted, bool wasRow) + { + Debug.Assert(slotDeleted >= 0, "Expected positive slotDeleted."); + + // Take care of the non-visible loaded rows + for (int index = 0; index < _loadedRows.Count;) + { + DataGridRow dataGridRow = _loadedRows[index]; + if (this.IsSlotVisible(dataGridRow.Slot)) + { + index++; + } + else + { + if (dataGridRow.Slot > slotDeleted) + { + CorrectRowAfterDeletion(dataGridRow, wasRow); + index++; + } + else if (dataGridRow.Slot == slotDeleted) + { + _loadedRows.RemoveAt(index); + } + else + { + index++; + } + } + } + + // Take care of the non-visible edited row + this.CorrectEditingRow(); + + // Take care of the non-visible focused row + if (_focusedRow != null && + _focusedRow != this.EditingRow && + !this.IsSlotVisible(_focusedRow.Slot) && + _focusedRow.Slot > slotDeleted) + { + CorrectRowAfterDeletion(_focusedRow, wasRow); + _focusedRow.EnsureBackground(); + _focusedRow.EnsureForeground(); + } + + // Take care of the visible rows + foreach (DataGridRow row in this.DisplayData.GetScrollingElements(true /*onlyRows*/)) + { + if (row.Slot > slotDeleted) + { + CorrectRowAfterDeletion(row, wasRow); + row.EnsureBackground(); + row.EnsureForeground(); + } + } + + // Update the RowGroupHeaders + foreach (int slot in this.RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo.Slot > slotDeleted) + { + rowGroupInfo.Slot--; + } + + if (rowGroupInfo.LastSubItemSlot >= slotDeleted) + { + rowGroupInfo.LastSubItemSlot--; + } + } + + // Update which row we've calculated the RowHeightEstimate up to + if (_lastEstimatedRow >= slotDeleted) + { + _lastEstimatedRow--; + } + } + + /// + /// Adjusts the index of all displayed, loaded and edited rows after rows were deleted. + /// + private void CorrectSlotsAfterInsertion(int slotInserted, bool isCollapsed, bool rowInserted) + { + Debug.Assert(slotInserted >= 0, "Expected positive slotInserted."); + + // Take care of the non-visible loaded rows + foreach (DataGridRow dataGridRow in _loadedRows) + { + if (!this.IsSlotVisible(dataGridRow.Slot) && dataGridRow.Slot >= slotInserted) + { + DataGrid.CorrectRowAfterInsertion(dataGridRow, rowInserted); + } + } + + // Take care of the non-visible focused row + if (_focusedRow != null && + _focusedRow != EditingRow && + !(this.IsSlotVisible(_focusedRow.Slot) || ((_focusedRow.Slot == slotInserted) && isCollapsed)) && + _focusedRow.Slot >= slotInserted) + { + DataGrid.CorrectRowAfterInsertion(_focusedRow, rowInserted); + _focusedRow.EnsureBackground(); + _focusedRow.EnsureForeground(); + } + + // Take care of the visible rows + foreach (DataGridRow row in this.DisplayData.GetScrollingElements(true /*onlyRows*/)) + { + if (row.Slot >= slotInserted) + { + DataGrid.CorrectRowAfterInsertion(row, rowInserted); + row.EnsureBackground(); + row.EnsureForeground(); + } + } + + // Re-calculate the EditingRow's Slot and Index and ensure that it is still selected. + this.CorrectEditingRow(); + + // Update the RowGroupHeaders + foreach (int slot in this.RowGroupHeadersTable.GetIndexes(slotInserted)) + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo.Slot >= slotInserted) + { + rowGroupInfo.Slot++; + } + + // We are purposefully checking GT and not GTE because the equality case is handled + // by the CorrectLastSubItemSlotsAfterInsertion method. + if (rowGroupInfo.LastSubItemSlot > slotInserted) + { + rowGroupInfo.LastSubItemSlot++; + } + } + + // Update which row we've calculated the RowHeightEstimate up to. + if (_lastEstimatedRow >= slotInserted) + { + _lastEstimatedRow++; + } + } + + private int CountAndPopulateGroupHeaders(object group, int rootSlot, int level) + { + int treeCount = 1; + + ICollectionViewGroup collectionViewGroup = group as ICollectionViewGroup; + if (collectionViewGroup != null) + { + if (collectionViewGroup.GroupItems != null && collectionViewGroup.GroupItems.Count > 0) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + HookupCollectionChangedListenerToGroup(collectionViewGroup.GroupItems as INotifyCollectionChanged); +#else + HookupVectorChangedListenerToGroup(collectionViewGroup.GroupItems); +#endif + if (collectionViewGroup.GroupItems[0] is ICollectionViewGroup) + { + foreach (object subGroup in collectionViewGroup.GroupItems) + { + treeCount += CountAndPopulateGroupHeaders(subGroup, rootSlot + treeCount, level + 1); + } + } + else + { + // Optimization: don't walk to the bottom level nodes + treeCount += collectionViewGroup.GroupItems.Count; + } + } + + this.RowGroupHeadersTable.AddValue(rootSlot, new DataGridRowGroupInfo(collectionViewGroup, Visibility.Visible, level, rootSlot, rootSlot + treeCount - 1)); + } + + return treeCount; + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + private void CollectionViewGroup_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // If we receive this event when the number of GroupDescriptions is different than what we have already + // accounted for, that means the ICollectionView is still in the process of updating its groups. It will + // send a reset notification when it's done, at which point we can update our visuals. + if (_rowGroupHeightsByLevel != null && this.DataConnection.CollectionView != null) + { + if (this.DataConnection.CollectionView.GroupDescriptions != null && this.DataConnection.CollectionView.GroupDescriptions.Count == _rowGroupHeightsByLevel.Length) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + this.CollectionViewGroup_CollectionChanged_Add(sender, e); + break; + case NotifyCollectionChangedAction.Remove: + this.CollectionViewGroup_CollectionChanged_Remove(sender, e); + break; + } + } + } + } + + private void CollectionViewGroup_CollectionChanged_Add(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null && e.NewItems.Count > 0) + { + OnCollectionViewGroupItemInserted(sender, e.NewItems[0], e.NewStartingIndex); + } + } + + private void CollectionViewGroup_CollectionChanged_Remove(object sender, NotifyCollectionChangedEventArgs e) + { + Debug.Assert(e.OldItems.Count == 1, "Expected e.OldItems.Count equals 1."); + if (e.OldItems != null && e.OldItems.Count > 0) + { + OnCollectionViewGroupItemRemoved(sender, e.OldItems[0], e.OldStartingIndex); + } + } +#else + private void CollectionViewGroupItems_VectorChanged(IObservableVector groupItems, IVectorChangedEventArgs e) + { + int index = (int)e.Index; + switch (e.CollectionChange) + { + case CollectionChange.ItemChanged: + break; + case CollectionChange.ItemInserted: + OnCollectionViewGroupItemInserted(groupItems, groupItems[index], index); + break; + case CollectionChange.ItemRemoved: + OnCollectionViewGroupItemRemoved(groupItems, groupItems[index], index); + break; + case CollectionChange.Reset: + break; + } + } +#endif + + private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + { + if (IsSlotVisible(parentGroupInfo.Slot)) + { + DataGridRowGroupHeader ancestorGroupHeader = this.DisplayData.GetDisplayedElement(parentGroupInfo.Slot) as DataGridRowGroupHeader; + while (ancestorGroupHeader != null) + { + ancestorGroupHeader.EnsureExpanderButtonIsChecked(); + if (ancestorGroupHeader.Level > 0) + { + int slot = this.RowGroupHeadersTable.GetPreviousIndex(ancestorGroupHeader.RowGroupInfo.Slot); + if (IsSlotVisible(slot)) + { + ancestorGroupHeader = this.DisplayData.GetDisplayedElement(slot) as DataGridRowGroupHeader; + continue; + } + } + + break; + } + } + } + + private void EnsureRowDetailsVisibility( + DataGridRow row, + bool raiseNotification) + { + // Show or hide RowDetails based on DataGrid settings + row.SetDetailsVisibilityInternal( + GetRowDetailsVisibility(row.Index), + raiseNotification); + } + + private IEnumerable GetAllRows() + { + if (_rowsPresenter != null) + { + foreach (UIElement element in _rowsPresenter.Children) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + yield return row; + } + } + } + } + + private IEnumerable GetAllRowGroupHeaders() + { + if (_rowsPresenter != null) + { + foreach (UIElement element in _rowsPresenter.Children) + { + DataGridRowGroupHeader rowGroupHeader = element as DataGridRowGroupHeader; + if (rowGroupHeader != null) + { + yield return rowGroupHeader; + } + } + } + } + + // Returns the number of rows with details visible between lowerBound and upperBound exclusive. + // As of now, the caller needs to account for Collapsed slots. This method assumes everything + // is visible + private int GetDetailsCountInclusive(int lowerBound, int upperBound) + { + // Convert from slots to indexes. + lowerBound = this.RowGroupHeadersTable.GetNextGap(lowerBound - 1); + upperBound = this.RowGroupHeadersTable.GetPreviousGap(upperBound + 1); + + lowerBound = this.RowIndexFromSlot(lowerBound); + upperBound = this.RowIndexFromSlot(upperBound); + + int indexCount = upperBound - lowerBound + 1; + if (indexCount <= 0) + { + return 0; + } + + if (this.RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible) + { + // Total rows minus ones which explicitly turned details off + return indexCount - _showDetailsTable.GetIndexCount(lowerBound, upperBound, Visibility.Collapsed); + } + else if (this.RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Collapsed) + { + // Total rows with details explicitly turned on + return _showDetailsTable.GetIndexCount(lowerBound, upperBound, Visibility.Visible); + } + else if (this.RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected) + { + // Total number of remaining rows that are selected + return _selectedItems.GetIndexCount(lowerBound, upperBound); + } + + Debug.Assert(false, "Expected known RowDetailsVisibilityMode value."); // Shouldn't ever happen + return 0; + } + + private void EnsureRowGroupSpacerColumn() + { + bool spacerColumnChanged = this.ColumnsInternal.EnsureRowGrouping(!this.RowGroupHeadersTable.IsEmpty); + if (spacerColumnChanged) + { + if (this.ColumnsInternal.RowGroupSpacerColumn.IsRepresented && this.CurrentColumnIndex == 0) + { + this.CurrentColumn = this.ColumnsInternal.FirstVisibleNonFillerColumn; + } + + ProcessFrozenColumnCount(this); + } + } + + private void EnsureRowGroupSpacerColumnWidth(int groupLevelCount) + { + if (groupLevelCount == 0) + { + this.ColumnsInternal.RowGroupSpacerColumn.Width = new DataGridLength(0); + } + else + { + this.ColumnsInternal.RowGroupSpacerColumn.Width = new DataGridLength(this.RowGroupSublevelIndents[groupLevelCount - 1]); + } + } + + private void EnsureRowGroupVisibility(DataGridRowGroupInfo rowGroupInfo, Visibility visibility, bool setCurrent) + { + if (rowGroupInfo == null) + { + return; + } + + if (rowGroupInfo.Visibility != visibility) + { + if (this.IsSlotVisible(rowGroupInfo.Slot)) + { + DataGridRowGroupHeader rowGroupHeader = this.DisplayData.GetDisplayedElement(rowGroupInfo.Slot) as DataGridRowGroupHeader; + Debug.Assert(rowGroupHeader != null, "Expected non-null rowGroupHeader."); + rowGroupHeader.ToggleExpandCollapse(visibility, setCurrent); + } + else + { + if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot)) + { + // Somewhere up the parent chain, there's a collapsed header so all the slots remain the same and + // we just need to mark this header with the new visibility + rowGroupInfo.Visibility = visibility; + } + else + { + if (rowGroupInfo.Slot < this.DisplayData.FirstScrollingSlot) + { + double heightChange = UpdateRowGroupVisibility(rowGroupInfo, visibility, false /*isHeaderDisplayed*/); + + // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative + // the user can still scroll to the top since the offset is non-zero + SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange)); + + this.DisplayData.FullyRecycleElements(); + } + else + { + UpdateRowGroupVisibility(rowGroupInfo, visibility, false /*isHeaderDisplayed*/); + } + + UpdateVerticalScrollBar(); + } + } + } + } + + // Expands slots from startSlot to endSlot inclusive and adds the amount expanded in this suboperation to + // the given totalHeightChanged of the entire operation + private void ExpandSlots(int startSlot, int endSlot, bool isHeaderDisplayed, ref int slotsExpanded, ref double totalHeightChange) + { + double heightAboveStartSlot = 0; + if (isHeaderDisplayed) + { + int slot = this.DisplayData.FirstScrollingSlot; + while (slot < startSlot) + { + heightAboveStartSlot += GetExactSlotElementHeight(slot); + slot = GetNextVisibleSlot(slot); + } + + // First make the bottom rows available for recycling so we minimize element creation when expanding + for (int i = 0; (i < endSlot - startSlot + 1) && (this.DisplayData.LastScrollingSlot > endSlot); i++) + { + RemoveDisplayedElement(this.DisplayData.LastScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + } + } + + // Figure out which slots actually need to be expanded since some might already be collapsed + double currentHeightChange = 0; + int firstSlot = startSlot; + int lastSlot; + while (firstSlot <= endSlot) + { + firstSlot = _collapsedSlotsTable.GetNextIndex(firstSlot - 1); + if (firstSlot == -1) + { + break; + } + + lastSlot = Math.Min(endSlot, _collapsedSlotsTable.GetNextGap(firstSlot) - 1); + + if (firstSlot <= lastSlot) + { + if (!isHeaderDisplayed) + { + // Estimate the height change if the slots aren't displayed. If they are displayed, we can add real values + double rowCount = lastSlot - firstSlot + 1; + rowCount -= GetRowGroupHeaderCount(firstSlot, lastSlot, Visibility.Collapsed, out var headerHeight); + double detailsCount = GetDetailsCountInclusive(firstSlot, lastSlot); + currentHeightChange += headerHeight + (detailsCount * this.RowDetailsHeightEstimate) + (rowCount * this.RowHeightEstimate); + } + + slotsExpanded += lastSlot - firstSlot + 1; + firstSlot = lastSlot + 1; + } + } + + // Update _collapsedSlotsTable in one bulk operation + _collapsedSlotsTable.RemoveValues(startSlot, endSlot - startSlot + 1); + + if (isHeaderDisplayed) + { + double availableHeight = this.CellsHeight - heightAboveStartSlot; + + // Actually expand the displayed slots up to what we can display + int lastExpandedSlot = -1; + for (int i = startSlot; (i <= endSlot) && (currentHeightChange < availableHeight); i++) + { + FrameworkElement insertedElement = InsertDisplayedElement(i, false /*updateSlotInformation*/); + lastExpandedSlot = i; + currentHeightChange += insertedElement.EnsureMeasured().DesiredSize.Height; + if (i > this.DisplayData.LastScrollingSlot) + { + this.DisplayData.LastScrollingSlot = i; + } + } + + // We were unable to expand the slots from (lastExpandedSlot + 1) to endSlot because we ran out of space; + // however, we also have extra visible elements below endSlot. In this case, we need to remove the + // extra elements. While we remove these, we need to mark (lastExpandedSlot + 1) to endSlot collapsed + // because that is a temporary gap that is not accounted for. + if ((lastExpandedSlot != -1) && (lastExpandedSlot < endSlot) && (this.DisplayData.LastScrollingSlot > endSlot)) + { + // Temporarily account for the slots we couldn't expand by marking them collapsed + _collapsedSlotsTable.AddValues(lastExpandedSlot + 1, endSlot - lastExpandedSlot, Visibility.Collapsed); + + // Remove the extra elements below our lastExpandedSlot + RemoveNonDisplayedRows(this.DisplayData.FirstScrollingSlot, lastExpandedSlot); + + // Remove the temporarily marked collapsed rows + _collapsedSlotsTable.RemoveValues(lastExpandedSlot + 1, endSlot - lastExpandedSlot); + } + } + + // Update the total height for the entire Expand operation + totalHeightChange += currentHeightChange; + } + + /// + /// Creates all the editing elements for the current editing row, so the bindings + /// all exist during validation. + /// + private void GenerateEditingElements() + { + if (this.EditingRow != null && this.EditingRow.Cells != null) + { + Debug.Assert(this.EditingRow.Cells.Count == this.ColumnsItemsInternal.Count, "Expected EditingRow.Cells.Count equals this.ColumnsItemsInternal.Count."); + foreach (DataGridColumn column in this.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && !c.IsReadOnly)) + { + column.GenerateEditingElementInternal(this.EditingRow.Cells[column.Index], this.EditingRow.DataContext); + } + } + } + + /// + /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event. + /// + /// A row for the provided index. + private DataGridRow GenerateRow(int rowIndex, int slot) + { + return GenerateRow(rowIndex, slot, this.DataConnection.GetDataItem(rowIndex)); + } + + /// + /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event. + /// + /// A row for the provided index. + private DataGridRow GenerateRow(int rowIndex, int slot, object dataContext) + { + Debug.Assert(rowIndex >= 0, "Expected positive rowIndex."); + DataGridRow dataGridRow = GetGeneratedRow(dataContext); + if (dataGridRow == null) + { + dataGridRow = this.DisplayData.GetUsedRow() ?? new DataGridRow(); + dataGridRow.Index = rowIndex; + dataGridRow.Slot = slot; + dataGridRow.OwningGrid = this; + dataGridRow.DataContext = dataContext; + CompleteCellsCollection(dataGridRow); + + OnLoadingRow(new DataGridRowEventArgs(dataGridRow)); + + DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as DataGridAutomationPeer; + if (peer != null) + { + peer.UpdateRowPeerEventsSource(dataGridRow); + } + } + + return dataGridRow; + } + + private DataGridRowGroupHeader GenerateRowGroupHeader(int slot, DataGridRowGroupInfo rowGroupInfo) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(rowGroupInfo != null, "Expected non-null rowGroupInfo."); + + DataGridRowGroupHeader groupHeader = this.DisplayData.GetUsedGroupHeader() ?? new DataGridRowGroupHeader(); + groupHeader.OwningGrid = this; + groupHeader.RowGroupInfo = rowGroupInfo; + groupHeader.DataContext = rowGroupInfo.CollectionViewGroup; + groupHeader.Level = rowGroupInfo.Level; + +#if FEATURE_ICOLLECTIONVIEW_GROUP + Debug.Assert(this.DataConnection.CollectionView != null && groupHeader.Level < this.DataConnection.CollectionView.GroupDescriptions.Count); + PropertyGroupDescription propertyGroupDescription = this.DataConnection.CollectionView.GroupDescriptions[groupHeader.Level] as PropertyGroupDescription; + if (propertyGroupDescription != null) + { + groupHeader.PropertyName = propertyGroupDescription.PropertyName; + } + + // Listen for CollectionViewGroup.PropertyChanged in order to update Title when ItemCount changes + if (rowGroupInfo.CollectionViewGroup != null) + { + INotifyPropertyChanged inpc = rowGroupInfo.CollectionViewGroup as INotifyPropertyChanged; + if (inpc != null && !_groupsPropertyChangedListenersTable.ContainsKey(inpc)) + { + WeakEventListener weakPropertyChangedListener = new WeakEventListener(this); + weakPropertyChangedListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionViewGroup_PropertyChanged(source, eventArgs); + weakPropertyChangedListener.OnDetachAction = (weakEventListener) => inpc.PropertyChanged -= weakEventListener.OnEvent; + inpc.PropertyChanged += weakPropertyChangedListener.OnEvent; + + _groupsPropertyChangedListenersTable.Add(inpc, weakPropertyChangedListener); + } + } +#endif + + OnLoadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + + if (!string.IsNullOrWhiteSpace(groupHeader.PropertyName) && + string.IsNullOrEmpty(groupHeader.PropertyValue) && + rowGroupInfo.CollectionViewGroup.GroupItems != null && + rowGroupInfo.CollectionViewGroup.GroupItems.Count > 0) + { + object propertyValue = TypeHelper.GetNestedPropertyValue(rowGroupInfo.CollectionViewGroup.GroupItems[0], groupHeader.PropertyName); + + if (propertyValue != null) + { + groupHeader.PropertyValue = propertyValue.ToString(); + } + } + + groupHeader.UpdateTitleElements(); + + DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as DataGridAutomationPeer; + if (peer != null) + { + peer.UpdateRowGroupHeaderPeerEventsSource(groupHeader); + } + + return groupHeader; + } + + /// + /// Returns the exact row height, whether it is currently displayed or not. + /// The row is generated and added to the displayed rows in case it is not already displayed. + /// The horizontal gridlines thickness are added. + /// + /// Exact row height with gridlines thickness. + private double GetExactSlotElementHeight(int slot) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(slot < this.SlotCount, "Expected slot smaller than SlotCount."); + + if (this.IsSlotVisible(slot)) + { + Debug.Assert(this.DisplayData.GetDisplayedElement(slot) != null, "Expected non-null DisplayData.GetDisplayedElement(slot)."); + return this.DisplayData.GetDisplayedElement(slot).EnsureMeasured().DesiredSize.Height; + } + + // InsertDisplayedElement automatically measures the element + FrameworkElement slotElement = InsertDisplayedElement(slot, true /*updateSlotInformation*/); + Debug.Assert(slotElement != null, "Expected non-null slotElement."); + return slotElement.DesiredSize.Height; + } + + // Returns an estimate for the height of the slots between fromSlot and toSlot + private double GetHeightEstimate(int fromSlot, int toSlot) + { + double rowCount = toSlot - fromSlot + 1; + rowCount -= GetRowGroupHeaderCount(fromSlot, toSlot, Visibility.Visible, out var headerHeight); + double detailsCount = GetDetailsCountInclusive(fromSlot, toSlot); + + return headerHeight + (detailsCount * this.RowDetailsHeightEstimate) + (rowCount * this.RowHeightEstimate); + } + + private DataGridRowGroupInfo GetParentGroupInfo(object collection) + { + if (collection == this.DataConnection.CollectionView.CollectionGroups) + { + // If the new item is a root level element, it has no parent group, so create an empty RowGroupInfo + return new DataGridRowGroupInfo(null, Visibility.Visible, -1, -1, -1); + } + else + { + foreach (int slot in this.RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo groupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (groupInfo.CollectionViewGroup.GroupItems == collection) + { + return groupInfo; + } + } + } + + return null; + } + + // Returns the inclusive count of expanded RowGroupHeaders from startSlot to endSlot + private int GetRowGroupHeaderCount(int startSlot, int endSlot, Visibility? visibility, out double headersHeight) + { + int count = 0; + headersHeight = 0; + foreach (int slot in this.RowGroupHeadersTable.GetIndexes(startSlot)) + { + if (slot > endSlot) + { + return count; + } + + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (!visibility.HasValue || + (visibility == Visibility.Visible && !_collapsedSlotsTable.Contains(slot)) || + (visibility == Visibility.Collapsed && _collapsedSlotsTable.Contains(slot))) + { + count++; + headersHeight += _rowGroupHeightsByLevel[rowGroupInfo.Level]; + } + } + + return count; + } + + /// + /// If the provided slot is displayed, returns the exact height. + /// If the slot is not displayed, returns a default height. + /// + /// Exact height of displayed slot, or default height otherwise. + private double GetSlotElementHeight(int slot) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(slot < this.SlotCount, "Expected slot smaller than SlotCount."); + if (this.IsSlotVisible(slot)) + { + Debug.Assert(this.DisplayData.GetDisplayedElement(slot) != null, "Expected non-null DisplayData.GetDisplayedElement(slot)."); + return this.DisplayData.GetDisplayedElement(slot).EnsureMeasured().DesiredSize.Height; + } + else + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo != null) + { + return _rowGroupHeightsByLevel[rowGroupInfo.Level]; + } + + // Assume it's a row since we're either not grouping or it wasn't a RowGroupHeader. + return this.RowHeightEstimate + (GetRowDetailsVisibility(this.RowIndexFromSlot(slot)) == Visibility.Visible ? this.RowDetailsHeightEstimate : 0); + } + } + + /// + /// Cumulates the approximate height of the non-collapsed slots from fromSlot to toSlot inclusive. + /// Including the potential gridline thickness. + /// + /// Cumulated approximate height of the non-collapsed slots from fromSlot to toSlot inclusive including the potential gridline thickness. + private double GetSlotElementsHeight(int fromSlot, int toSlot) + { + Debug.Assert(toSlot >= fromSlot, "Expected toSlot greater or equal to fromSlot."); + + double height = 0; + for (int slot = GetNextVisibleSlot(fromSlot - 1); slot <= toSlot; slot = GetNextVisibleSlot(slot)) + { + height += GetSlotElementHeight(slot); + } + + return height; + } + + /// + /// Checks if the row for the provided dataContext has been generated and is present + /// in either the loaded rows, pre-fetched rows, or editing row. + /// The displayed rows are *not* searched. Returns null if the row does not belong to those 3 categories. + /// + /// Either a loaded, or pre-fetched or editing row. + private DataGridRow GetGeneratedRow(object dataContext) + { + // Check the list of rows being loaded via the LoadingRow event. + DataGridRow dataGridRow = GetLoadedRow(dataContext); + if (dataGridRow != null) + { + return dataGridRow; + } + + // Check the potential editing row. + if (this.EditingRow != null && dataContext == this.EditingRow.DataContext) + { + return this.EditingRow; + } + + // Check the potential focused row. + if (_focusedRow != null && dataContext == _focusedRow.DataContext) + { + return _focusedRow; + } + + return null; + } + + private DataGridRow GetLoadedRow(object dataContext) + { + foreach (DataGridRow dataGridRow in _loadedRows) + { + if (dataGridRow.DataContext == dataContext) + { + return dataGridRow; + } + } + + return null; + } + + private FrameworkElement InsertDisplayedElement(int slot, bool updateSlotInformation) + { + FrameworkElement slotElement; + if (this.RowGroupHeadersTable.Contains(slot)) + { + slotElement = GenerateRowGroupHeader(slot, this.RowGroupHeadersTable.GetValueAt(slot) /*rowGroupInfo*/); + } + else + { + // If we're grouping, the GroupLevel needs to be fixed later by methods calling this + // which end up inserting rows. We don't do it here because elements could be inserted + // from top to bottom or bottom to up so it's better to do in one pass + slotElement = GenerateRow(RowIndexFromSlot(slot), slot); + } + + InsertDisplayedElement(slot, slotElement, false /*wasNewlyAdded*/, updateSlotInformation); + return slotElement; + } + + private void InsertDisplayedElement(int slot, UIElement element, bool wasNewlyAdded, bool updateSlotInformation) + { + // We can only support creating new rows that are adjacent to the currently visible rows + // since they need to be added to the visual tree for us to Measure them. + Debug.Assert( + this.DisplayData.FirstScrollingSlot == -1 || (slot >= GetPreviousVisibleSlot(this.DisplayData.FirstScrollingSlot) && slot <= GetNextVisibleSlot(this.DisplayData.LastScrollingSlot)), + "Expected DisplayData.FirstScrollingSlot equals -1 or (slot greater than or equal to GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot) and slot smaller than or equal to GetNextVisibleSlot(DisplayData.LastScrollingSlot))."); + Debug.Assert(element != null, "Expected non-null element."); + + if (_rowsPresenter != null) + { + DataGridRowGroupHeader groupHeader = null; + DataGridRow row = element as DataGridRow; + if (row != null) + { + LoadRowVisualsForDisplay(row); + + if (IsRowRecyclable(row)) + { + if (!row.IsRecycled) + { + Debug.Assert(!_rowsPresenter.Children.Contains(element), "Expected element not contained in _rowsPresenter.Children."); + _rowsPresenter.Children.Add(row); + } + } + else + { + element.Clip = null; + Debug.Assert(row.Index == RowIndexFromSlot(slot), "Expected row.Index equals RowIndexFromSlot(slot)."); + if (!_rowsPresenter.Children.Contains(row)) + { + _rowsPresenter.Children.Add(row); + } + } + } + else + { + groupHeader = element as DataGridRowGroupHeader; + Debug.Assert(groupHeader != null, "Expected non-null grouHeader."); + if (groupHeader != null) + { + groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : this.RowGroupSublevelIndents[groupHeader.Level - 1]; + if (!groupHeader.IsRecycled) + { + _rowsPresenter.Children.Add(element); + } + + groupHeader.LoadVisualsForDisplay(); + + Style lastStyle = _rowGroupHeaderStyles.Count > 0 ? _rowGroupHeaderStyles[_rowGroupHeaderStyles.Count - 1] : null; + EnsureElementStyle(groupHeader, groupHeader.Style, groupHeader.Level < _rowGroupHeaderStyles.Count ? _rowGroupHeaderStyles[groupHeader.Level] : lastStyle); + } + } + + // Measure the element and update AvailableRowRoom + element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + this.AvailableSlotElementRoom -= element.DesiredSize.Height; + + if (groupHeader != null) + { + _rowGroupHeightsByLevel[groupHeader.Level] = groupHeader.DesiredSize.Height; + } + + if (row != null && this.RowHeightEstimate == DataGrid.DATAGRID_defaultRowHeight && double.IsNaN(row.Height)) + { + this.RowHeightEstimate = row.DesiredSize.Height - row.DetailsContentHeight; + } + } + + if (wasNewlyAdded) + { + this.DisplayData.CorrectSlotsAfterInsertion(slot, element, false /*isCollapsed*/); + } + else + { + this.DisplayData.LoadScrollingSlot(slot, element, updateSlotInformation); + } + } + + private void InsertElement(int slot, UIElement element, bool updateVerticalScrollBarOnly, bool isCollapsed, bool isRow) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(slot <= this.SlotCount, "Expected slot smaller than or equal to SlotCount."); + + OnInsertingElement(slot, true /*firstInsertion*/, isCollapsed, isRow); // will throw an exception if the insertion is illegal + + OnInsertedElement_Phase1(slot, element, isCollapsed, isRow); + this.SlotCount++; + if (!isCollapsed) + { + this.VisibleSlotCount++; + } + + OnInsertedElement_Phase2(slot, updateVerticalScrollBarOnly, isCollapsed); + } + + private void InvalidateRowHeightEstimate() + { + // Start from scratch and assume that we haven't estimated any rows + _lastEstimatedRow = -1; + } + + private void OnAddedElement_Phase1(int slot, UIElement element) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + + // Row needs to be potentially added to the displayed rows + if (SlotIsDisplayed(slot)) + { + InsertDisplayedElement(slot, element, true /*wasNewlyAdded*/, true); + } + } + + private void OnAddedElement_Phase2(int slot, bool updateVerticalScrollBarOnly) + { + if (slot < this.DisplayData.FirstScrollingSlot - 1) + { + // The element was added above our viewport so it pushes the VerticalOffset down + double elementHeight = this.RowGroupHeadersTable.Contains(slot) ? this.RowGroupHeaderHeightEstimate : this.RowHeightEstimate; + + SetVerticalOffset(_verticalOffset + elementHeight); + } + + if (updateVerticalScrollBarOnly) + { + UpdateVerticalScrollBar(); + } + else + { + ComputeScrollBarsLayout(); + + // Reposition rows in case we use a recycled one + InvalidateRowsArrange(); + } + } + + private void OnCollectionViewGroupItemInserted(object groupItems, object insertedItem, int insertedIndex) + { + // We need to figure out the CollectionViewGroup that the sender belongs to. We could cache + // it by tagging the collections ahead of time, but I think the extra storage might not be worth + // it since this lookup should be performant enough + int insertSlot = -1; + DataGridRowGroupInfo parentGroupInfo = GetParentGroupInfo(groupItems); + ICollectionViewGroup group = insertedItem as ICollectionViewGroup; + + if (parentGroupInfo != null) + { + if (group != null || parentGroupInfo.Level == -1) + { + insertSlot = parentGroupInfo.Slot + 1; + + // For groups, we need to skip over subgroups to find the correct slot + DataGridRowGroupInfo groupInfo; + for (int i = 0; i < insertedIndex; i++) + { + do + { + insertSlot = this.RowGroupHeadersTable.GetNextIndex(insertSlot); + groupInfo = this.RowGroupHeadersTable.GetValueAt(insertSlot); + } + while (groupInfo != null && groupInfo.Level > parentGroupInfo.Level + 1); + + if (groupInfo == null) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + // We couldn't find the subchild so this should go at the end + // if it's the placeholder, or second from the end if it's a new group or item. + if (this.DataConnection.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd && + this.DataConnection.IndexOf(e.NewItems[0]) != this.DataConnection.NewItemPlaceholderIndex && + this.SlotCount > 0) + { + insertSlot = this.SlotCount - 1; + } + else +#endif + { + insertSlot = this.SlotCount; + } + } + } + } + else + { + // For items the slot is a simple calculation + insertSlot = parentGroupInfo.Slot + insertedIndex + 1; + } + } + + if (insertSlot != -1) + { + bool isCollapsed = parentGroupInfo != null && (parentGroupInfo.Visibility == Visibility.Collapsed || _collapsedSlotsTable.Contains(parentGroupInfo.Slot)); + if (group != null) + { + if (group.GroupItems != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + HookupCollectionChangedListenerToGroup(group.GroupItems as INotifyCollectionChanged); +#else + HookupVectorChangedListenerToGroup(group.GroupItems); +#endif + } + + DataGridRowGroupInfo newGroupInfo = new DataGridRowGroupInfo(group, Visibility.Visible, parentGroupInfo.Level + 1, insertSlot, insertSlot); + InsertElementAt(insertSlot, -1 /*rowIndex*/, null /*item*/, newGroupInfo, isCollapsed); + this.RowGroupHeadersTable.AddValue(insertSlot, newGroupInfo); + } + else + { + // Assume we're adding a new row + int rowIndex = this.DataConnection.IndexOf(insertedItem); + Debug.Assert(rowIndex != -1, "Expected rowIndex other than -1."); + if (this.SlotCount == 0 && this.DataConnection.ShouldAutoGenerateColumns) + { + AutoGenerateColumnsPrivate(); + } + + InsertElementAt(insertSlot, rowIndex, insertedItem /*item*/, null /*rowGroupInfo*/, isCollapsed); + } + + CorrectLastSubItemSlotsAfterInsertion(parentGroupInfo); + if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1) + { + // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed + EnsureAnscestorsExpanderButtonChecked(parentGroupInfo); + } + } + } + + private void OnCollectionViewGroupItemRemoved(object groupItems, object removedItem, int removedIndex) + { + ICollectionViewGroup removedGroup = removedItem as ICollectionViewGroup; + if (removedGroup != null) + { + if (removedGroup.GroupItems != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + UnhookCollectionChangedListenerFromGroup(removedGroup.GroupItems as INotifyCollectionChanged, true /*removeFromTable*/); +#else + UnhookVectorChangedListenerFromGroup(removedGroup.GroupItems, true /*removeFromTable*/); +#endif + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + WeakEventListener weakPropertyChangedListener; + INotifyPropertyChanged inpc = removedGroup as INotifyPropertyChanged; + if (inpc != null && _groupsPropertyChangedListenersTable.TryGetValue(inpc, out weakPropertyChangedListener)) + { + weakPropertyChangedListener.Detach(); + _groupsPropertyChangedListenersTable.Remove(inpc); + } +#else +#endif + + DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(removedGroup); + Debug.Assert(groupInfo != null, "Expected non-null groupInfo."); + if ((groupInfo.Level == _rowGroupHeightsByLevel.Length - 1) && (removedGroup.GroupItems != null) && (removedGroup.GroupItems.Count > 0)) + { + Debug.Assert(groupInfo.LastSubItemSlot - groupInfo.Slot == removedGroup.GroupItems.Count, "Expected groupInfo.LastSubItemSlot - groupInfo.Slot equals removedGroup.GroupItems.Count."); + + // If we're removing a leaf Group then remove all of its items before removing the Group. + for (int i = 0; i < removedGroup.GroupItems.Count; i++) + { + RemoveElementAt(groupInfo.Slot + 1, removedGroup.GroupItems[i] /*item*/, true /*isRow*/); + } + } + + RemoveElementAt(groupInfo.Slot, null /*item*/, false /*isRow*/); + } + else + { + // A single item was removed from a leaf group + DataGridRowGroupInfo parentGroupInfo = GetParentGroupInfo(groupItems); + if (parentGroupInfo != null) + { + int slot; + if (parentGroupInfo.CollectionViewGroup == null && this.RowGroupHeadersTable.IndexCount > 0) + { +#if FEATURE_IEDITABLECOLLECTIONVIEW + // In this case, we're removing from the root group. If there are other groups, then this must + // be either the new item row or the placeholder that doesn't belong to any group. + if (this.DataConnection.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd && this.SlotCount > 1) + { + slot = this.SlotCount - 2; + } + else +#endif + { + slot = this.SlotCount - 1; + } + } + else + { + slot = parentGroupInfo.Slot + removedIndex + 1; + } + + RemoveElementAt(slot, removedItem, true /*isRow*/); + } + } + } + + private void OnElementsChanged(bool grew) + { + if (grew && + this.ColumnsItemsInternal.Count > 0 && + this.CurrentColumnIndex == -1) + { + MakeFirstDisplayedCellCurrentCell(); + } + } + + private void OnInsertedElement_Phase1(int slot, UIElement element, bool isCollapsed, bool isRow) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + + // Fix the Index of all following rows + CorrectSlotsAfterInsertion(slot, isCollapsed, isRow); + + // Next, same effect as adding a row + if (element != null) + { +#if DEBUG + DataGridRow dataGridRow = element as DataGridRow; + if (dataGridRow != null) + { + Debug.Assert(dataGridRow.Cells.Count == this.ColumnsItemsInternal.Count, "Expected dataGridRow.Cells.Count equals ColumnsItemsInternal.Count."); + + int columnIndex = 0; + foreach (DataGridCell dataGridCell in dataGridRow.Cells) + { + Debug.Assert(dataGridCell.OwningRow == dataGridRow, "Expected dataGridRow owns dataGridCell."); + Debug.Assert(dataGridCell.OwningColumn == this.ColumnsItemsInternal[columnIndex], "Expected ColumnsItemsInternal[columnIndex] owns dataGridCell."); + columnIndex++; + } + } +#endif + Debug.Assert(!isCollapsed, "Expected isCollapsed is false."); + OnAddedElement_Phase1(slot, element); + } + else if ((slot <= this.DisplayData.FirstScrollingSlot) || (isCollapsed && (slot <= this.DisplayData.LastScrollingSlot))) + { + this.DisplayData.CorrectSlotsAfterInsertion(slot, null /*row*/, isCollapsed); + } + } + + private void OnInsertedElement_Phase2(int slot, bool updateVerticalScrollBarOnly, bool isCollapsed) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + + if (!isCollapsed) + { + // Same effect as adding a row + OnAddedElement_Phase2(slot, updateVerticalScrollBarOnly); + } + } + + private void OnInsertingElement(int slotInserted, bool firstInsertion, bool isCollapsed, bool isRow) + { + // Reset the current cell's address if it's after the inserted row. + if (firstInsertion) + { + if (this.CurrentSlot != -1 && slotInserted <= this.CurrentSlot) + { + // The underlying data was already added, therefore we need to avoid accessing any back-end data since we might be off by 1 row. + _temporarilyResetCurrentCell = true; + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success, "Expected successful SetCurrentCellCore call."); + } + } + + // Update the slot ranges for the RowGroupHeaders before updating the _selectedItems table, + // because it's dependent on the slots being correct with regards to grouping. + this.RowGroupHeadersTable.InsertIndex(slotInserted); + + _selectedItems.InsertIndex(slotInserted); + if (isRow) + { + // Since details are only visible for rows, the showDetailsTable only contains row indexes. + int rowIndex = this.RowIndexFromSlot(slotInserted); + if (rowIndex == this.DataConnection.NewItemPlaceholderIndex) + { + _showDetailsTable.InsertIndexAndValue(rowIndex, Visibility.Collapsed); + } + else + { + _showDetailsTable.InsertIndex(rowIndex); + } + } + + if (isCollapsed) + { + _collapsedSlotsTable.InsertIndexAndValue(slotInserted, Visibility.Collapsed); + } + else + { + _collapsedSlotsTable.InsertIndex(slotInserted); + } + + // If we've inserted rows before the current selected item, update its index + if (slotInserted <= this.SelectedIndex) + { + this.SetValueNoCallback(SelectedIndexProperty, this.SelectedIndex + 1); + } + } + + private void OnRemovedElement(int slotDeleted, object itemDeleted, bool isRow) + { + this.SlotCount--; + bool wasCollapsed = _collapsedSlotsTable.Contains(slotDeleted); + if (!wasCollapsed) + { + this.VisibleSlotCount--; + } + + // If we're deleting the focused row, we need to clear the cached value + if (_focusedRow != null && _focusedRow.DataContext == itemDeleted) + { + ResetFocusedRow(); + } + + // The element needs to be potentially removed from the displayed elements + UIElement elementDeleted = null; + if (slotDeleted <= this.DisplayData.LastScrollingSlot) + { + if ((slotDeleted >= this.DisplayData.FirstScrollingSlot) && !wasCollapsed) + { + elementDeleted = this.DisplayData.GetDisplayedElement(slotDeleted); + + // Make sure we have the correct height for the calculation below + elementDeleted.EnsureMeasured(); + + // We need to retrieve the Element before updating the tables, but we need + // to update the tables before updating DisplayData in RemoveDisplayedElement + UpdateTablesForRemoval(slotDeleted, itemDeleted); + + // Displayed row is removed + RemoveDisplayedElement(elementDeleted, slotDeleted, true /*wasDeleted*/, true /*updateSlotInformation*/); + } + else + { + UpdateTablesForRemoval(slotDeleted, itemDeleted); + + // Removed row is not in view, just update the DisplayData + this.DisplayData.CorrectSlotsAfterDeletion(slotDeleted, wasCollapsed); + } + } + else + { + // The element was removed beyond the viewport so we just need to update the tables + UpdateTablesForRemoval(slotDeleted, itemDeleted); + } + + // If a row was removed before the currently selected row, update its index + if (slotDeleted < this.SelectedIndex) + { + this.SetValueNoCallback(SelectedIndexProperty, this.SelectedIndex - 1); + } + + if (!wasCollapsed) + { + if (slotDeleted >= this.DisplayData.LastScrollingSlot && elementDeleted == null) + { + // Deleted Row is below our Viewport, we just need to adjust the scrollbar + UpdateVerticalScrollBar(); + } + else + { + double verticalOffsetAdjustment = 0; + if (elementDeleted != null) + { + // Deleted Row is within our Viewport, update the AvailableRowRoom + this.AvailableSlotElementRoom += elementDeleted.DesiredSize.Height; + + // When we delete a row in view, we also need to adjust the verticalOffset + // in the cases where the deletion causes us to be scrolled further down than + // what is possible. + double newVerticalScrollBarMax = _vScrollBar.Maximum - elementDeleted.DesiredSize.Height; + if (_verticalOffset > newVerticalScrollBarMax) + { + verticalOffsetAdjustment = elementDeleted.DesiredSize.Height; + } + } + else + { + // Deleted element is above our Viewport, update the vertical offset + verticalOffsetAdjustment = isRow ? this.RowHeightEstimate : this.RowGroupHeaderHeightEstimate; + } + + if (verticalOffsetAdjustment > 0) + { + SetVerticalOffset(Math.Max(0, _verticalOffset - verticalOffsetAdjustment)); + + // If we've adjusted the vertical offset so that is less than the amount that the first element + // is covered up, we need to uncover the first element appropriately. + if (this.NegVerticalOffset > _verticalOffset) + { + this.NegVerticalOffset = _verticalOffset; + } + } + + ComputeScrollBarsLayout(); + + // Reposition rows in case we use a recycled one + InvalidateRowsArrange(); + } + } + } + + private void OnRemovingElement(int slotDeleted) + { + // Note that the row needs to be deleted no matter what. The underlying data row was already deleted. + Debug.Assert(slotDeleted >= 0, "Expected positive slotDeleted."); + Debug.Assert(slotDeleted < this.SlotCount, "Expected slotDeleted smaller than SlotCount."); + _temporarilyResetCurrentCell = false; + + // Reset the current cell's address if it's on the deleted row, or after it. + if (this.CurrentSlot != -1 && slotDeleted <= this.CurrentSlot) + { + _desiredCurrentColumnIndex = this.CurrentColumnIndex; + if (slotDeleted == this.CurrentSlot) + { + // No editing is committed since the underlying entity was already deleted. + bool success = SetCurrentCellCore(-1, -1, false /*commitEdit*/, false /*endRowEdit*/); + Debug.Assert(success, "Expected successful SetCurrentCellCore call."); + } + else + { + // Underlying data of deleted row is gone. It cannot be accessed anymore. Skip the commit of the editing. + _temporarilyResetCurrentCell = true; + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success, "Expected successful SetCurrentCellCore call."); + } + } + } + + // Makes sure the row shows the proper visuals for selection, currency, details, etc. + private void LoadRowVisualsForDisplay(DataGridRow row) + { + // If the row has been recycled, reapply the BackgroundBrush + if (row.IsRecycled) + { + row.EnsureBackground(); + row.EnsureForeground(); + row.ApplyCellsState(false /*animate*/); + } + else if (row == this.EditingRow) + { + row.ApplyCellsState(false /*animate*/); + } + + // Set the Row's Style if we one's defined at the DataGrid level and the user didn't + // set one at the row level + EnsureElementStyle(row, null, this.RowStyle); + row.EnsureHeaderStyleAndVisibility(null); + + // Check to see if the row contains the CurrentCell, apply its state. + if (this.CurrentColumnIndex != -1 && + this.CurrentSlot != -1 && + row.Index == this.CurrentSlot) + { + row.Cells[this.CurrentColumnIndex].ApplyCellState(false /*animate*/); + } + + if (row.IsSelected || row.IsRecycled) + { + row.ApplyState(false); + } + + // Show or hide RowDetails based on DataGrid settings + EnsureRowDetailsVisibility( + row, + false /*raiseNotification*/); + } + + private void PopulateRowGroupHeadersTable() + { + if (this.DataConnection.CollectionView != null && +#if FEATURE_ICOLLECTIONVIEW_GROUP + this.DataConnection.CollectionView.CanGroup && +#endif + this.DataConnection.CollectionView.CollectionGroups != null) + { + int totalSlots = 0; + _topLevelGroup = this.DataConnection.CollectionView.CollectionGroups; +#if FEATURE_ICOLLECTIONVIEW_GROUP + HookupCollectionChangedListenerToGroup(_topLevelGroup as INotifyCollectionChanged); +#else + HookupVectorChangedListenerToGroup(_topLevelGroup); +#endif + foreach (object group in this.DataConnection.CollectionView.CollectionGroups) + { + totalSlots += CountAndPopulateGroupHeaders(group, totalSlots, 0); + } + } +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (this.IsReadOnly && this.DataConnection.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd) + { + _collapsedSlotsTable.AddValue(this.SlotFromRowIndex(this.DataConnection.NewItemPlaceholderIndex), Visibility.Collapsed); + } +#endif + } + +#if FEATURE_ICOLLECTIONVIEW_GROUP + private void HookupCollectionChangedListenerToGroup(INotifyCollectionChanged incc) + { + if (incc != null && !_groupsCollectionChangedListenersTable.ContainsKey(incc)) + { + WeakEventListener weakCollectionChangedListener = new WeakEventListener(this); + weakCollectionChangedListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionViewGroup_CollectionChanged(source, eventArgs); + weakCollectionChangedListener.OnDetachAction = (weakEventListener) => incc.CollectionChanged -= weakCollectionChangedListener.OnEvent; + incc.CollectionChanged += weakCollectionChangedListener.OnEvent; + + _groupsCollectionChangedListenersTable.Add(incc, weakCollectionChangedListener); + } + } + + private void UnhookCollectionChangedListenerFromGroup(INotifyCollectionChanged incc, bool removeFromTable) + { + WeakEventListener weakCollectionChangedListener; + if (incc != null && _groupsCollectionChangedListenersTable.TryGetValue(incc, out weakCollectionChangedListener)) + { + weakCollectionChangedListener.Detach(); + if (removeFromTable) + { + _groupsCollectionChangedListenersTable.Remove(incc); + } + } + } +#else + private void HookupVectorChangedListenerToGroup(IObservableVector groupItems) + { + if (groupItems != null && !_groupsVectorChangedListenersTable.ContainsKey(groupItems)) + { + WeakEventListener weakVectorChangedListener = new WeakEventListener(this); + weakVectorChangedListener.OnEventAction = (instance, source, eventArgs) => instance.CollectionViewGroupItems_VectorChanged(source as IObservableVector, eventArgs); + weakVectorChangedListener.OnDetachAction = (weakEventListener) => groupItems.VectorChanged -= weakVectorChangedListener.OnEvent; + groupItems.VectorChanged += weakVectorChangedListener.OnEvent; + + _groupsVectorChangedListenersTable.Add(groupItems, weakVectorChangedListener); + } + } + + private void UnhookVectorChangedListenerFromGroup(IObservableVector groupItems, bool removeFromTable) + { + WeakEventListener weakVectorChangedListener; + if (groupItems != null && _groupsVectorChangedListenersTable.TryGetValue(groupItems, out weakVectorChangedListener)) + { + weakVectorChangedListener.Detach(); + if (removeFromTable) + { + _groupsVectorChangedListenersTable.Remove(groupItems); + } + } + } +#endif + + private void RefreshRowGroupHeaders() + { + if (this.DataConnection.CollectionView != null && +#if FEATURE_ICOLLECTIONVIEW_GROUP + this.DataConnection.CollectionView.CanGroup && +#endif + this.DataConnection.CollectionView.CollectionGroups != null) + { + // Initialize our array for the height of the RowGroupHeaders by Level. + // If the Length is the same, we can reuse the old array. +#if FEATURE_ICOLLECTIONVIEW_GROUP + int groupLevelCount = this.DataConnection.CollectionView.GroupDescriptions.Count; +#else + int groupLevelCount = 1; +#endif + if (_rowGroupHeightsByLevel == null || _rowGroupHeightsByLevel.Length != groupLevelCount) + { + _rowGroupHeightsByLevel = new double[groupLevelCount]; + for (int i = 0; i < groupLevelCount; i++) + { + // Default height for now, the actual heights are updated as the RowGroupHeaders + // are added and measured + _rowGroupHeightsByLevel[i] = DATAGRID_defaultRowHeight; + } + } + + if (this.RowGroupSublevelIndents == null || this.RowGroupSublevelIndents.Length != groupLevelCount) + { + this.RowGroupSublevelIndents = new double[groupLevelCount]; + double indent; + for (int i = 0; i < groupLevelCount; i++) + { + DataGridRowGroupHeader rowGroupHeader = null; + indent = DATAGRID_defaultRowGroupSublevelIndent; + if (i < this.RowGroupHeaderStyles.Count && this.RowGroupHeaderStyles[i] != null) + { + if (rowGroupHeader == null) + { + rowGroupHeader = new DataGridRowGroupHeader(); + } + + rowGroupHeader.Style = this.RowGroupHeaderStyles[i]; + if (rowGroupHeader.SublevelIndent != DataGrid.DATAGRID_defaultRowGroupSublevelIndent) + { + indent = rowGroupHeader.SublevelIndent; + } + } + + this.RowGroupSublevelIndents[i] = indent; + if (i > 0) + { + this.RowGroupSublevelIndents[i] += this.RowGroupSublevelIndents[i - 1]; + } + } + } + + EnsureRowGroupSpacerColumnWidth(groupLevelCount); + } + } + + private void RefreshSlotCounts() + { + this.SlotCount = this.DataConnection.Count; + this.SlotCount += this.RowGroupHeadersTable.IndexCount; + this.VisibleSlotCount = this.SlotCount - _collapsedSlotsTable.GetIndexCount(0, this.SlotCount - 1); + } + + private void RemoveDisplayedElement(int slot, bool wasDeleted, bool updateSlotInformation) + { + Debug.Assert(slot >= this.DisplayData.FirstScrollingSlot, "Expected slot larger or equal to DisplayData.FirstScrollingSlot."); + Debug.Assert(slot <= this.DisplayData.LastScrollingSlot, "Expected slot smaller or equal to DisplayData.LastScrollingSlot."); + + RemoveDisplayedElement(this.DisplayData.GetDisplayedElement(slot), slot, wasDeleted, updateSlotInformation); + } + + // Removes an element from display either because it was deleted or it was scrolled out of view. + // If the element was provided, it will be the element removed; otherwise, the element will be + // retrieved from the slot information + private void RemoveDisplayedElement(UIElement element, int slot, bool wasDeleted, bool updateSlotInformation) + { + DataGridRow dataGridRow = element as DataGridRow; + if (dataGridRow != null) + { + if (IsRowRecyclable(dataGridRow)) + { + UnloadRow(dataGridRow); + } + else + { + dataGridRow.Clip = new RectangleGeometry(); + } + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + this.DisplayData.AddRecylableRowGroupHeader(groupHeader); + } + else if (_rowsPresenter != null) + { + _rowsPresenter.Children.Remove(element); + } + } + + // Update DisplayData + if (wasDeleted) + { + this.DisplayData.CorrectSlotsAfterDeletion(slot, false /*wasCollapsed*/); + } + else + { + this.DisplayData.UnloadScrollingElement(slot, updateSlotInformation, false /*wasDeleted*/); + } + } + + /// + /// Removes all of the editing elements for the row that is just leaving editing mode. + /// + private void RemoveEditingElements() + { + if (this.EditingRow != null && this.EditingRow.Cells != null) + { + Debug.Assert(this.EditingRow.Cells.Count == this.ColumnsItemsInternal.Count, "Expected EditingRow.Cells.Count equals ColumnsItemsInternal.Count."); + foreach (DataGridColumn column in this.Columns) + { + column.RemoveEditingElement(); + } + } + } + + private void RemoveElementAt(int slot, object item, bool isRow) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + Debug.Assert(slot < this.SlotCount, "Expected slot smaller than SlotCount."); + + OnRemovingElement(slot); + + CorrectSlotsAfterDeletion(slot, isRow); + + OnRemovedElement(slot, item, isRow); + + // Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem + // and CurrentSlot with the currently edited cell, since OnRemovingElement called + // SetCurrentCellCore(-1, -1) to temporarily reset the current cell. + if (_temporarilyResetCurrentCell && + _editingColumnIndex != -1 && + _previousCurrentItem != null && + this.EditingRow != null && + this.EditingRow.Slot != -1) + { + ProcessSelectionAndCurrency( + columnIndex: _editingColumnIndex, + item: _previousCurrentItem, + backupSlot: this.EditingRow.Slot, + action: DataGridSelectionAction.None, + scrollIntoView: false); + } + } + + private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot) + { + while (this.DisplayData.FirstScrollingSlot < newFirstDisplayedSlot) + { + // Need to add rows above the lastDisplayedScrollingRow + RemoveDisplayedElement(this.DisplayData.FirstScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + } + + while (this.DisplayData.LastScrollingSlot > newLastDisplayedSlot) + { + // Need to remove rows below the lastDisplayedScrollingRow + RemoveDisplayedElement(this.DisplayData.LastScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + } + } + + private void ResetDisplayedRows() + { + if (this.UnloadingRow != null || this.UnloadingRowGroup != null) + { + foreach (UIElement element in this.DisplayData.GetScrollingElements()) + { + // Raise Unloading Row for all the rows we're displaying + DataGridRow row = element as DataGridRow; + if (row != null) + { + if (IsRowRecyclable(row)) + { + OnUnloadingRow(new DataGridRowEventArgs(row)); + } + } + else + { + // Raise Unloading Row for all the RowGroupHeaders we're displaying + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + } + } + } + } + + this.DisplayData.ClearElements(true /*recycleRows*/); + this.AvailableSlotElementRoom = this.CellsHeight; + } + + /// + /// Determines whether the row at the provided index must be displayed or not. + /// + /// True when the slot is displayed. + private bool SlotIsDisplayed(int slot) + { + Debug.Assert(slot >= 0, "Expected positive slot."); + + if (slot >= this.DisplayData.FirstScrollingSlot && + slot <= this.DisplayData.LastScrollingSlot) + { + // Additional row takes the spot of a displayed row - it is necessarily displayed + return true; + } + else if (this.DisplayData.FirstScrollingSlot == -1 && + this.CellsHeight > 0 && + this.CellsWidth > 0) + { + return true; + } + else if (slot == GetNextVisibleSlot(this.DisplayData.LastScrollingSlot)) + { + if (this.AvailableSlotElementRoom > 0) + { + // There is room for this additional row + return true; + } + } + + return false; + } + + // Updates display information and displayed rows after scrolling the given number of pixels + private void ScrollSlotsByHeight(double height) + { + Debug.Assert(this.DisplayData.FirstScrollingSlot >= 0, "Expected positive DisplayData.FirstScrollingSlot."); + Debug.Assert(!DoubleUtil.IsZero(height), "DoubleUtil.IsZero(height) is false."); + + _scrollingByHeight = true; + try + { + double deltaY = 0; + int newFirstScrollingSlot = this.DisplayData.FirstScrollingSlot; + double newVerticalOffset = _verticalOffset + height; + if (height > 0) + { + // Scrolling Down + int lastVisibleSlot = GetPreviousVisibleSlot(this.SlotCount); + if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset)) + { + // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom + // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by + // OnRowMeasure(Size). For most data, this should be unnoticeable. + ResetDisplayedRows(); + UpdateDisplayedRowsFromBottom(lastVisibleSlot); + newFirstScrollingSlot = this.DisplayData.FirstScrollingSlot; + } + else + { + deltaY = GetSlotElementHeight(newFirstScrollingSlot) - this.NegVerticalOffset; + if (DoubleUtil.LessThan(height, deltaY)) + { + // We've merely covered up more of the same row we're on + this.NegVerticalOffset += height; + } + else + { + // Figure out what row we've scrolled down to and update the value for this.NegVerticalOffset + this.NegVerticalOffset = 0; + + if (height > 2 * this.CellsHeight && + (this.RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || this.RowDetailsTemplate == null)) + { + // Very large scroll occurred. Instead of determining the exact number of scrolled off rows, + // let's estimate the number based on this.RowHeight. + ResetDisplayedRows(); + double singleRowHeightEstimate = this.RowHeightEstimate + (this.RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible ? this.RowDetailsHeightEstimate : 0); + int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate); + scrolledToSlot += _collapsedSlotsTable.GetIndexCount(newFirstScrollingSlot, newFirstScrollingSlot + scrolledToSlot); + newFirstScrollingSlot = Math.Min(GetNextVisibleSlot(scrolledToSlot), lastVisibleSlot); + } + else + { + while (DoubleUtil.LessThanOrClose(deltaY, height)) + { + if (newFirstScrollingSlot < lastVisibleSlot) + { + if (this.IsSlotVisible(newFirstScrollingSlot)) + { + // Make the top row available for reuse + RemoveDisplayedElement(newFirstScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + } + + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + } + else + { + // We're being told to scroll beyond the last row, ignore the extra + this.NegVerticalOffset = 0; + break; + } + + double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + double remainingHeight = height - deltaY; + if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight)) + { + deltaY += rowHeight; + } + else + { + this.NegVerticalOffset = remainingHeight; + break; + } + } + } + } + } + } + else + { + // Scrolling Up + if (DoubleUtil.GreaterThanOrClose(height + this.NegVerticalOffset, 0)) + { + // We've merely exposing more of the row we're on + this.NegVerticalOffset += height; + } + else + { + // Figure out what row we've scrolled up to and update the value for this.NegVerticalOffset + deltaY = -this.NegVerticalOffset; + this.NegVerticalOffset = 0; + + if (height < -2 * this.CellsHeight && + (this.RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || this.RowDetailsTemplate == null)) + { + // Very large scroll occurred. Instead of determining the exact number of scrolled off rows, + // let's estimate the number based on this.RowHeight. + if (newVerticalOffset == 0) + { + newFirstScrollingSlot = 0; + } + else + { + double singleRowHeightEstimate = this.RowHeightEstimate + (this.RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible ? this.RowDetailsHeightEstimate : 0); + int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate); + scrolledToSlot -= _collapsedSlotsTable.GetIndexCount(scrolledToSlot, newFirstScrollingSlot); + + newFirstScrollingSlot = Math.Max(0, GetPreviousVisibleSlot(scrolledToSlot + 1)); + } + + ResetDisplayedRows(); + } + else + { + int lastScrollingSlot = this.DisplayData.LastScrollingSlot; + while (DoubleUtil.GreaterThan(deltaY, height)) + { + if (newFirstScrollingSlot > 0) + { + if (this.IsSlotVisible(lastScrollingSlot)) + { + // Make the bottom row available for reuse + RemoveDisplayedElement(lastScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + lastScrollingSlot = GetPreviousVisibleSlot(lastScrollingSlot); + } + + newFirstScrollingSlot = GetPreviousVisibleSlot(newFirstScrollingSlot); + } + else + { + this.NegVerticalOffset = 0; + break; + } + + double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + double remainingHeight = height - deltaY; + if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0)) + { + deltaY -= rowHeight; + } + else + { + this.NegVerticalOffset = rowHeight + remainingHeight; + break; + } + } + } + } + + if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) + { + // We've scrolled to the top of the ScrollBar, automatically place the user at the very top + // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. + // strategy. For most data, this should be unnoticeable. + ResetDisplayedRows(); + this.NegVerticalOffset = 0; + UpdateDisplayedRows(0, this.CellsHeight); + newFirstScrollingSlot = 0; + } + } + + double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + if (DoubleUtil.LessThan(firstRowHeight, this.NegVerticalOffset)) + { + // We've scrolled off more of the first row than what's possible. This can happen + // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling + // cleanup issue. In this case, simply try to display the next row as the first row instead + if (newFirstScrollingSlot < this.SlotCount - 1) + { + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + Debug.Assert(newFirstScrollingSlot != -1, "Expected newFirstScrollingSlot other than -1."); + } + + this.NegVerticalOffset = 0; + } + + UpdateDisplayedRows(newFirstScrollingSlot, this.CellsHeight); + + double firstElementHeight = GetExactSlotElementHeight(this.DisplayData.FirstScrollingSlot); + if (DoubleUtil.GreaterThan(this.NegVerticalOffset, firstElementHeight)) + { + int firstElementSlot = this.DisplayData.FirstScrollingSlot; + + // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element + while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(this.NegVerticalOffset, firstElementHeight)) + { + int previousSlot = GetPreviousVisibleSlot(firstElementSlot); + if (previousSlot == -1) + { + this.NegVerticalOffset = 0; + VerticalOffset = 0; + } + else + { + this.NegVerticalOffset -= firstElementHeight; + VerticalOffset = Math.Max(0, _verticalOffset - firstElementHeight); + firstElementSlot = previousSlot; + firstElementHeight = GetExactSlotElementHeight(firstElementSlot); + } + } + + // We could be smarter about this, but it's not common so we wouldn't gain much from optimizing here + if (firstElementSlot != this.DisplayData.FirstScrollingSlot) + { + UpdateDisplayedRows(firstElementSlot, this.CellsHeight); + } + } + + Debug.Assert(this.DisplayData.FirstScrollingSlot >= 0, "Expected positive DisplayData.FirstScrollingSlot."); + Debug.Assert(GetExactSlotElementHeight(this.DisplayData.FirstScrollingSlot) > this.NegVerticalOffset, "Expected GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) larger than this.NegVerticalOffset."); + + if (this.DisplayData.FirstScrollingSlot == 0) + { + VerticalOffset = this.NegVerticalOffset; + } + else if (DoubleUtil.GreaterThan(this.NegVerticalOffset, newVerticalOffset)) + { + // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb + // can stay in the same place + this.NegVerticalOffset = newVerticalOffset; + VerticalOffset = newVerticalOffset; + } + else + { + VerticalOffset = newVerticalOffset; + } + + Debug.Assert( + _verticalOffset != 0 || this.NegVerticalOffset != 0 || this.DisplayData.FirstScrollingSlot <= 0, + "Expected _verticalOffset other than 0 or this.NegVerticalOffset other than 0 or this.DisplayData.FirstScrollingSlot smaller than or equal to 0."); + + SetVerticalOffset(_verticalOffset); + + this.DisplayData.FullyRecycleElements(); + + Debug.Assert(DoubleUtil.GreaterThanOrClose(this.NegVerticalOffset, 0), "Expected NegVerticalOffset greater than or close to 0."); + Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, this.NegVerticalOffset), "Expected _verticalOffset greater than or close to NegVerticalOffset."); + + DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as DataGridAutomationPeer; + if (peer != null) + { + peer.RaiseAutomationScrollEvents(); + } + } + finally + { + _scrollingByHeight = false; + } + } + + private void SelectDisplayedElement(int slot) + { + Debug.Assert(IsSlotVisible(slot), "Expected IsSlotVisible(slot) is true."); + FrameworkElement element = this.DisplayData.GetDisplayedElement(slot) as FrameworkElement; + DataGridRow row = this.DisplayData.GetDisplayedElement(slot) as DataGridRow; + if (row != null) + { + row.ApplyState(true /*animate*/); + EnsureRowDetailsVisibility( + row, + true /*raiseNotification*/); + } + else + { + // Assume it's a RowGroupHeader. + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + groupHeader.ApplyState(true /*useTransitions*/); + } + } + + private void SelectSlot(int slot, bool isSelected) + { + _selectedItems.SelectSlot(slot, isSelected); + if (this.IsSlotVisible(slot)) + { + SelectDisplayedElement(slot); + } + } + + private void SelectSlots(int startSlot, int endSlot, bool isSelected) + { + _selectedItems.SelectSlots(startSlot, endSlot, isSelected); + + // Apply the correct row state for display rows and also expand or collapse detail accordingly + int firstSlot = Math.Max(this.DisplayData.FirstScrollingSlot, startSlot); + int lastSlot = Math.Min(this.DisplayData.LastScrollingSlot, endSlot); + + for (int slot = firstSlot; slot <= lastSlot; slot++) + { + if (IsSlotVisible(slot)) + { + SelectDisplayedElement(slot); + } + } + } + + private bool ToggleRowGroup() + { + if (!this.ColumnHeaderHasFocus && this.FirstVisibleSlot != -1 && this.RowGroupHeadersTable.Contains(this.CurrentSlot)) + { + ICollectionViewGroup collectionViewGroup = this.RowGroupHeadersTable.GetValueAt(this.CurrentSlot).CollectionViewGroup; + + if (collectionViewGroup != null) + { + DataGridRowGroupInfo dataGridRowGroupInfo = RowGroupInfoFromCollectionViewGroup(collectionViewGroup); + + if (dataGridRowGroupInfo != null) + { + if (dataGridRowGroupInfo.Visibility == Visibility.Collapsed) + { + ExpandRowGroup(collectionViewGroup, false /*expandAllSubgroups*/); + } + else + { + CollapseRowGroup(collectionViewGroup, false /*collapseAllSubgroups*/); + } + + return true; + } + } + } + + return false; + } + + private void UnloadElements(bool recycle) + { + // Since we're unloading all the elements, we can't be in editing mode anymore, + // so commit if we can, otherwise force cancel. + if (!this.CommitEdit()) + { + this.CancelEdit(DataGridEditingUnit.Row, false); + } + + this.ResetEditingRow(); + + // Make sure to clear the focused row (because it's no longer relevant). + if (_focusedRow != null) + { + ResetFocusedRow(); + Focus(FocusState.Programmatic); + } + + if (_rowsPresenter != null) + { + foreach (UIElement element in _rowsPresenter.Children) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + // Raise UnloadingRow for any row that was visible + if (this.IsSlotVisible(row.Slot)) + { + OnUnloadingRow(new DataGridRowEventArgs(row)); + } + + row.DetachFromDataGrid(recycle && row.IsRecyclable /*recycle*/); + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null && this.IsSlotVisible(groupHeader.RowGroupInfo.Slot)) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + } + } + } + + if (!recycle) + { + _rowsPresenter.Children.Clear(); + } + } + + this.DisplayData.ClearElements(recycle); + + // Update the AvailableRowRoom since we're displaying 0 rows now + this.AvailableSlotElementRoom = this.CellsHeight; + this.VisibleSlotCount = 0; + } + + private void UnloadRow(DataGridRow dataGridRow) + { + Debug.Assert(dataGridRow != null, "Expected non-null dataGridRow."); + Debug.Assert(_rowsPresenter != null, "Expected non-null _rowsPresenter."); + Debug.Assert(_rowsPresenter.Children.Contains(dataGridRow), "Expected dataGridRow contained in _rowsPresenter.Children."); + + if (_loadedRows.Contains(dataGridRow)) + { + return; // The row is still referenced, we can't release it. + } + + // Raise UnloadingRow regardless of whether the row will be recycled + OnUnloadingRow(new DataGridRowEventArgs(dataGridRow)); + + // TODO: Be able to recycle the current row + bool recycleRow = this.CurrentSlot != dataGridRow.Index; + + // Don't recycle if the row has a custom Style set + recycleRow &= dataGridRow.Style == null || dataGridRow.Style == this.RowStyle; + + if (recycleRow) + { + this.DisplayData.AddRecylableRow(dataGridRow); + } + else + { + // TODO: Should we raise Unloading for rows that are not going to be recycled as well + _rowsPresenter.Children.Remove(dataGridRow); + dataGridRow.DetachFromDataGrid(false); + } + } + + private void UpdateDisplayedRows(int newFirstDisplayedSlot, double displayHeight) + { + Debug.Assert(!_collapsedSlotsTable.Contains(newFirstDisplayedSlot), "Expected newFirstDisplayedSlot not contained in _collapsedSlotsTable."); + + int firstDisplayedScrollingSlot = newFirstDisplayedSlot; + int lastDisplayedScrollingSlot = -1; + double deltaY = -this.NegVerticalOffset; + int visibleScrollingRows = 0; + + if (DoubleUtil.LessThanOrClose(displayHeight, 0) || this.SlotCount == 0 || this.ColumnsItemsInternal.Count == 0) + { + return; + } + + if (firstDisplayedScrollingSlot == -1) + { + // 0 is fine because the element in the first slot cannot be collapsed + firstDisplayedScrollingSlot = 0; + } + + int slot = firstDisplayedScrollingSlot; + while (slot < this.SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight)) + { + deltaY += GetExactSlotElementHeight(slot); + visibleScrollingRows++; + lastDisplayedScrollingSlot = slot; + slot = GetNextVisibleSlot(slot); + } + + while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + { + slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); + if (slot >= 0) + { + deltaY += GetExactSlotElementHeight(slot); + firstDisplayedScrollingSlot = slot; + visibleScrollingRows++; + } + } + + // If we're up to the first row, and we still have room left, uncover as much of the first row as we can + if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight)) + { + double newNegVerticalOffset = Math.Max(0, this.NegVerticalOffset - displayHeight + deltaY); + deltaY += this.NegVerticalOffset - newNegVerticalOffset; + this.NegVerticalOffset = newNegVerticalOffset; + } + + if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(this.NegVerticalOffset, 0))) + { + this.DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; + } + else + { + this.DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows; + } + + if (visibleScrollingRows == 0) + { + firstDisplayedScrollingSlot = -1; + Debug.Assert(lastDisplayedScrollingSlot == -1, "Expected lastDisplayedScrollingSlot equal to -1."); + } + + Debug.Assert(lastDisplayedScrollingSlot < this.SlotCount, "lastDisplayedScrollingRow larger than number of rows"); + + RemoveNonDisplayedRows(firstDisplayedScrollingSlot, lastDisplayedScrollingSlot); + + Debug.Assert(this.DisplayData.NumDisplayedScrollingElements >= 0, "the number of visible scrolling rows can't be negative"); + Debug.Assert(this.DisplayData.NumTotallyDisplayedScrollingElements >= 0, "the number of totally visible scrolling rows can't be negative"); + Debug.Assert(this.DisplayData.FirstScrollingSlot < this.SlotCount, "firstDisplayedScrollingRow larger than number of rows"); + Debug.Assert(this.DisplayData.FirstScrollingSlot == firstDisplayedScrollingSlot, "Expected DisplayData.FirstScrollingSlot equal to firstDisplayedScrollingSlot."); + Debug.Assert(this.DisplayData.LastScrollingSlot == lastDisplayedScrollingSlot, "DisplayData.LastScrollingSlot equal to lastDisplayedScrollingSlot."); + } + + // Similar to UpdateDisplayedRows except that it starts with the LastDisplayedScrollingRow + // and computes the FirstDisplayScrollingRow instead of doing it the other way around. We use this + // when scrolling down to a full row + private void UpdateDisplayedRowsFromBottom(int newLastDisplayedScrollingRow) + { + Debug.Assert(!_collapsedSlotsTable.Contains(newLastDisplayedScrollingRow), "Expected newLastDisplayedScrollingRow not contained in _collapsedSlotsTable."); + + int lastDisplayedScrollingRow = newLastDisplayedScrollingRow; + int firstDisplayedScrollingRow = -1; + double displayHeight = this.CellsHeight; + double deltaY = 0; + int visibleScrollingRows = 0; + + if (DoubleUtil.LessThanOrClose(displayHeight, 0) || this.SlotCount == 0 || this.ColumnsItemsInternal.Count == 0) + { + this.ResetDisplayedRows(); + return; + } + + if (lastDisplayedScrollingRow == -1) + { + lastDisplayedScrollingRow = 0; + } + + int slot = lastDisplayedScrollingRow; + while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + { + deltaY += GetExactSlotElementHeight(slot); + visibleScrollingRows++; + firstDisplayedScrollingRow = slot; + slot = GetPreviousVisibleSlot(slot); + } + + this.DisplayData.NumTotallyDisplayedScrollingElements = deltaY > displayHeight ? visibleScrollingRows - 1 : visibleScrollingRows; + + Debug.Assert(this.DisplayData.NumTotallyDisplayedScrollingElements >= 0, "Expected positive DisplayData.NumTotallyDisplayedScrollingElements."); + Debug.Assert(lastDisplayedScrollingRow < this.SlotCount, "lastDisplayedScrollingRow larger than number of rows"); + + this.NegVerticalOffset = Math.Max(0, deltaY - displayHeight); + + RemoveNonDisplayedRows(firstDisplayedScrollingRow, lastDisplayedScrollingRow); + + Debug.Assert(this.DisplayData.NumDisplayedScrollingElements >= 0, "the number of visible scrolling rows can't be negative"); + Debug.Assert(this.DisplayData.NumTotallyDisplayedScrollingElements >= 0, "the number of totally visible scrolling rows can't be negative"); + Debug.Assert(this.DisplayData.FirstScrollingSlot < this.SlotCount, "firstDisplayedScrollingRow larger than number of rows"); + } + + private void UpdateRowDetailsHeightEstimate() + { + if (_rowsPresenter != null && _measured && this.RowDetailsTemplate != null) + { + FrameworkElement detailsContent = this.RowDetailsTemplate.LoadContent() as FrameworkElement; + if (detailsContent != null) + { + if (this.VisibleSlotCount > 0) + { + detailsContent.DataContext = this.DataConnection.GetDataItem(0); + } + + _rowsPresenter.Children.Add(detailsContent); + + detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + this.RowDetailsHeightEstimate = detailsContent.DesiredSize.Height; + _rowsPresenter.Children.Remove(detailsContent); + } + } + } + + // This method does not check the state of the parent RowGroupHeaders, it assumes they're ready for this newVisibility to + // be applied this header. + // Returns the number of pixels that were expanded or (collapsed); however, if we're expanding displayed rows, we only expand up + // to what we can display. + private double UpdateRowGroupVisibility(DataGridRowGroupInfo targetRowGroupInfo, Visibility newVisibility, bool isHeaderDisplayed) + { + double heightChange = 0; + int slotsExpanded = 0; + int startSlot = targetRowGroupInfo.Slot + 1; + int endSlot; + + targetRowGroupInfo.Visibility = newVisibility; + if (newVisibility == Visibility.Visible) + { + // Expand + foreach (int slot in this.RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1)) + { + if (slot >= startSlot) + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo.Level <= targetRowGroupInfo.Level) + { + break; + } + + if (rowGroupInfo.Visibility == Visibility.Collapsed) + { + // Skip over the items in collapsed subgroups + endSlot = rowGroupInfo.Slot; + ExpandSlots(startSlot, endSlot, isHeaderDisplayed, ref slotsExpanded, ref heightChange); + startSlot = rowGroupInfo.LastSubItemSlot + 1; + } + } + } + + if (targetRowGroupInfo.LastSubItemSlot >= startSlot) + { + ExpandSlots(startSlot, targetRowGroupInfo.LastSubItemSlot, isHeaderDisplayed, ref slotsExpanded, ref heightChange); + } + + if (isHeaderDisplayed) + { + UpdateDisplayedRows(this.DisplayData.FirstScrollingSlot, this.CellsHeight); + } + } + else + { + // Collapse + endSlot = this.SlotCount - 1; +#if FEATURE_IEDITABLECOLLECTIONVIEW + if (this.DataConnection.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtEnd) + { + endSlot--; + } +#endif + + if (this.DataConnection.IsAddingNew) + { + endSlot--; + } + + Debug.Assert(endSlot >= 0, "Expected positive endSlot."); + foreach (int slot in this.RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1)) + { + DataGridRowGroupInfo rowGroupInfo = this.RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo.Level <= targetRowGroupInfo.Level) + { + endSlot = slot - 1; + break; + } + } + + int oldLastDisplayedSlot = this.DisplayData.LastScrollingSlot; + + // onlyChildrenDisplayed is true if the RowGroupHeader is not displayed but some of its children are + bool onlyChildrenDisplayed = !isHeaderDisplayed && (endSlot >= this.DisplayData.FirstScrollingSlot); + double partialGroupDisplayedHeight = 0; + if (isHeaderDisplayed || onlyChildrenDisplayed) + { + // If the RowGroupHeader is displayed or its children are partially displayed, + // we need to remove all the displayed slots that aren't already collapsed + int startDisplayedSlot = Math.Max(startSlot, this.DisplayData.FirstScrollingSlot); + int endDisplayedSlot = Math.Min(endSlot, this.DisplayData.LastScrollingSlot); + + int elementsToRemove = endDisplayedSlot - startDisplayedSlot + 1 - _collapsedSlotsTable.GetIndexCount(startDisplayedSlot, endDisplayedSlot); + if (_focusedRow != null && _focusedRow.Slot >= startSlot && _focusedRow.Slot <= endSlot) + { + Debug.Assert(this.EditingRow == null, "Expected null EditingRow."); + + // Don't call ResetFocusedRow here because we're already cleaning it up below, and we don't want to FullyRecycle yet + _focusedRow = null; + } + + for (int i = 0; i < elementsToRemove; i++) + { + UIElement displayedElement = this.DisplayData.GetDisplayedElement(startDisplayedSlot); + displayedElement.EnsureMeasured(); + + // For partially displayed groups, we need to update the slot information right away. For groups + // where the RowGroupHeader is displayed, we can just mark them collapsed later. + RemoveDisplayedElement(displayedElement, startDisplayedSlot, false /*wasDeleted*/, onlyChildrenDisplayed /*updateSlotInformation*/); + if (onlyChildrenDisplayed) + { + startDisplayedSlot = this.DisplayData.FirstScrollingSlot; + partialGroupDisplayedHeight += displayedElement.DesiredSize.Height; + } + } + + if (onlyChildrenDisplayed) + { + partialGroupDisplayedHeight -= this.NegVerticalOffset; + } + } + + // If part of the group we collapsed was partially displayed, we only collapsed the amount that was not displayed. + heightChange += partialGroupDisplayedHeight; + + double heightChangeBelowLastDisplayedSlot = 0; + if (this.DisplayData.FirstScrollingSlot >= startSlot && this.DisplayData.FirstScrollingSlot <= endSlot) + { + // Our first visible slot was collapsed, find the replacement + int collapsedSlotsAbove = this.DisplayData.FirstScrollingSlot - startSlot - _collapsedSlotsTable.GetIndexCount(startSlot, this.DisplayData.FirstScrollingSlot); + Debug.Assert(collapsedSlotsAbove > 0, "Expected positive collapsedSlotsAbove."); + int newFirstScrollingSlot = GetNextVisibleSlot(this.DisplayData.FirstScrollingSlot); + while (collapsedSlotsAbove > 1 && newFirstScrollingSlot < this.SlotCount) + { + collapsedSlotsAbove--; + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + } + + heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, ref heightChangeBelowLastDisplayedSlot); + if (isHeaderDisplayed || onlyChildrenDisplayed) + { + if (newFirstScrollingSlot >= this.SlotCount) + { + // No visible slots below, look up + UpdateDisplayedRowsFromBottom(targetRowGroupInfo.Slot); + } + else + { + UpdateDisplayedRows(newFirstScrollingSlot, this.CellsHeight); + } + } + } + else + { + heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, ref heightChangeBelowLastDisplayedSlot); + } + + if (this.DisplayData.LastScrollingSlot >= startSlot && this.DisplayData.LastScrollingSlot <= endSlot) + { + // Collapsed the last scrolling row, we need to update it + this.DisplayData.LastScrollingSlot = GetPreviousVisibleSlot(this.DisplayData.LastScrollingSlot); + } + + // Collapsing could cause the vertical offset to move up if we collapsed a lot of slots + // near the bottom of the DataGrid. To do this, we compare the height we collapsed to + // the distance to the last visible row and adjust the scrollbar if we collapsed more + if (isHeaderDisplayed && _verticalOffset > 0) + { + int lastVisibleSlot = GetPreviousVisibleSlot(this.SlotCount); + int slot = GetNextVisibleSlot(oldLastDisplayedSlot); + + // AvailableSlotElementRoom ends up being the amount of the last slot that is partially scrolled off + // as a negative value, heightChangeBelowLastDisplayed slot is also a negative value since we're collapsing + double heightToLastVisibleSlot = this.AvailableSlotElementRoom + heightChangeBelowLastDisplayedSlot; + while ((heightToLastVisibleSlot > heightChange) && (slot < lastVisibleSlot)) + { + heightToLastVisibleSlot -= GetSlotElementHeight(slot); + slot = GetNextVisibleSlot(slot); + } + + if (heightToLastVisibleSlot > heightChange) + { + double newVerticalOffset = _verticalOffset + heightChange - heightToLastVisibleSlot; + if (newVerticalOffset > 0) + { + SetVerticalOffset(newVerticalOffset); + } + else + { + // Collapsing causes the vertical offset to go to 0 so we should go back to the first row. + ResetDisplayedRows(); + this.NegVerticalOffset = 0; + SetVerticalOffset(0); + int firstDisplayedRow = GetNextVisibleSlot(-1); + UpdateDisplayedRows(firstDisplayedRow, this.CellsHeight); + } + } + } + } + + // Update VisibleSlotCount + this.VisibleSlotCount += slotsExpanded; + + return heightChange; + } + + private void UpdateTablesForRemoval(int slotDeleted, object itemDeleted) + { + if (this.RowGroupHeadersTable.Contains(slotDeleted)) + { + // A RowGroupHeader was removed + this.RowGroupHeadersTable.RemoveIndexAndValue(slotDeleted); + _collapsedSlotsTable.RemoveIndexAndValue(slotDeleted); + _selectedItems.DeleteSlot(slotDeleted); + } + else + { + // Update the ranges of selected rows + if (_selectedItems.ContainsSlot(slotDeleted)) + { + this.SelectionHasChanged = true; + } + + _selectedItems.Delete(slotDeleted, itemDeleted); + _showDetailsTable.RemoveIndex(RowIndexFromSlot(slotDeleted)); + this.RowGroupHeadersTable.RemoveIndex(slotDeleted); + _collapsedSlotsTable.RemoveIndex(slotDeleted); + } + } + +#if DEBUG + internal void PrintRowGroupInfo() + { + Debug.WriteLine("-----------------------------------------------RowGroupHeaders"); + foreach (int slot in this.RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo info = this.RowGroupHeadersTable.GetValueAt(slot); +#if FEATURE_ICOLLECTIONVIEW_GROUP + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} {1} Slot:{2} Last:{3} Level:{4}", info.CollectionViewGroup.Name, info.Visibility.ToString(), slot, info.LastSubItemSlot, info.Level)); +#else + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} Slot:{1} Last:{2} Level:{3}", info.Visibility.ToString(), slot, info.LastSubItemSlot, info.Level)); +#endif + } + + Debug.WriteLine("-----------------------------------------------CollapsedSlots"); + _collapsedSlotsTable.PrintIndexes(); + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowsPresenter.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowsPresenter.cs new file mode 100644 index 0000000..b9f3f55 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridRowsPresenter.cs @@ -0,0 +1,234 @@ +// 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; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Automation.Peers; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Primitives +{ + /// + /// Used within the template of a to specify the + /// location in the control's visual tree where the rows are to be added. + /// + public sealed class DataGridRowsPresenter : Panel + { + private double _preManipulationHorizontalOffset; + private double _preManipulationVerticalOffset; + + /// + /// Initializes a new instance of the class. + /// + public DataGridRowsPresenter() + { + this.ManipulationStarting += new ManipulationStartingEventHandler(DataGridRowsPresenter_ManipulationStarting); + this.ManipulationStarted += new ManipulationStartedEventHandler(DataGridRowsPresenter_ManipulationStarted); + this.ManipulationDelta += new ManipulationDeltaEventHandler(DataGridRowsPresenter_ManipulationDelta); + } + + internal DataGrid OwningGrid + { + get; + set; + } + + /// + /// Arranges the content of the . + /// + /// + /// The actual size used by the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + protected override Size ArrangeOverride(Size finalSize) + { + if (finalSize.Height == 0 || this.OwningGrid == null) + { + return base.ArrangeOverride(finalSize); + } + + this.OwningGrid.OnFillerColumnWidthNeeded(finalSize.Width); + + double rowDesiredWidth = this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth + this.OwningGrid.ColumnsInternal.FillerColumn.FillerWidth; + double topEdge = -this.OwningGrid.NegVerticalOffset; + foreach (UIElement element in this.OwningGrid.DisplayData.GetScrollingElements()) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + Debug.Assert(row.Index != -1, "Expected Index other than -1."); // A displayed row should always have its index + + // Visibility for all filler cells needs to be set in one place. Setting it individually in + // each CellsPresenter causes an NxN layout cycle (see DevDiv Bugs 211557) + row.EnsureFillerVisibility(); + row.Arrange(new Rect(-this.OwningGrid.HorizontalOffset, topEdge, rowDesiredWidth, element.DesiredSize.Height)); + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { + double leftEdge = this.OwningGrid.AreRowGroupHeadersFrozen ? 0 : -this.OwningGrid.HorizontalOffset; + groupHeader.Arrange(new Rect(leftEdge, topEdge, rowDesiredWidth - leftEdge, element.DesiredSize.Height)); + } + } + + topEdge += element.DesiredSize.Height; + } + + double finalHeight = Math.Max(topEdge + this.OwningGrid.NegVerticalOffset, finalSize.Height); + + // Clip the RowsPresenter so rows cannot overlap other elements in certain styling scenarios + RectangleGeometry rg = new RectangleGeometry(); + rg.Rect = new Rect(0, 0, finalSize.Width, finalHeight); + this.Clip = rg; + + return new Size(finalSize.Width, finalHeight); + } + + /// + /// Measures the children of a to + /// prepare for arranging them during the pass. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that child elements should not exceed. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + if (availableSize.Height == 0 || this.OwningGrid == null) + { + return base.MeasureOverride(availableSize); + } + + // If the Width of our RowsPresenter changed then we need to invalidate our rows + bool invalidateRows = + (!this.OwningGrid.RowsPresenterAvailableSize.HasValue || availableSize.Width != this.OwningGrid.RowsPresenterAvailableSize.Value.Width) && + !double.IsInfinity(availableSize.Width); + + // The DataGrid uses the RowsPresenter available size in order to autogrow + // and calculate the scrollbars + this.OwningGrid.RowsPresenterAvailableSize = availableSize; + + this.OwningGrid.OnRowsMeasure(); + + double totalHeight = -this.OwningGrid.NegVerticalOffset; + double totalCellsWidth = this.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth; + + double headerWidth = 0; + foreach (UIElement element in this.OwningGrid.DisplayData.GetScrollingElements()) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + if (invalidateRows) + { + row.InvalidateMeasure(); + } + } + + element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + if (row != null && row.HeaderCell != null) + { + headerWidth = Math.Max(headerWidth, row.HeaderCell.DesiredSize.Width); + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null && groupHeader.HeaderCell != null) + { + headerWidth = Math.Max(headerWidth, groupHeader.HeaderCell.DesiredSize.Width); + } + } + + totalHeight += element.DesiredSize.Height; + } + + this.OwningGrid.RowHeadersDesiredWidth = headerWidth; + + // Could be positive infinity depending on the DataGrid's bounds + this.OwningGrid.AvailableSlotElementRoom = availableSize.Height - totalHeight; + + // TODO: totalHeight can be negative if we've just collapsed details. This is a workaround, + // the real fix is to correct NegVerticalOffset. + totalHeight = Math.Max(0, totalHeight); + + return new Size(totalCellsWidth + headerWidth, totalHeight); + } + + /// + /// Creates AutomationPeer () + /// + /// An automation peer for this . + protected override AutomationPeer OnCreateAutomationPeer() + { + return new DataGridRowsPresenterAutomationPeer(this); + } + + private void DataGridRowsPresenter_ManipulationStarting(object sender, ManipulationStartingRoutedEventArgs e) + { + if (this.OwningGrid != null) + { + Debug.Assert(this.OwningGrid.IsEnabled, "Expected OwningGrid.IsEnabled is true."); + + _preManipulationHorizontalOffset = this.OwningGrid.HorizontalOffset; + _preManipulationVerticalOffset = this.OwningGrid.VerticalOffset; + } + } + + private void DataGridRowsPresenter_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + if (e.PointerDeviceType != Windows.Devices.Input.PointerDeviceType.Touch) + { + e.Complete(); + } + } + + private void DataGridRowsPresenter_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + if (this.OwningGrid != null) + { + e.Handled = + this.OwningGrid.ProcessScrollOffsetDelta(_preManipulationHorizontalOffset - e.Cumulative.Translation.X - this.OwningGrid.HorizontalOffset, true /*isForHorizontalScroll*/) || + this.OwningGrid.ProcessScrollOffsetDelta(_preManipulationVerticalOffset - e.Cumulative.Translation.Y - this.OwningGrid.VerticalOffset, false /*isForHorizontalScroll*/); + } + } + +#if DEBUG + internal void PrintChildren() + { + foreach (UIElement element in this.Children) + { + DataGridRow row = element as DataGridRow; + if (row != null) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Row: {1} Visibility: {2} ", row.Slot, row.Index, row.Visibility)); + } + else + { + DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader; + if (groupHeader != null) + { +#if FEATURE_ICOLLECTIONVIEW_GROUP + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} GroupHeader: {1} Visibility: {2}", groupHeader.RowGroupInfo.Slot, groupHeader.RowGroupInfo.CollectionViewGroup.Name, groupHeader.Visibility)); +#else + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "Slot: {0} Visibility: {1}", groupHeader.RowGroupInfo.Slot, groupHeader.Visibility)); +#endif + } + } + } + } +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridSelectedItemsCollection.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridSelectedItemsCollection.cs new file mode 100644 index 0000000..378dd1b --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridSelectedItemsCollection.cs @@ -0,0 +1,494 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + internal class DataGridSelectedItemsCollection : IList + { + private List _oldSelectedItemsCache; + private IndexToValueTable _oldSelectedSlotsTable; + private List _selectedItemsCache; + private IndexToValueTable _selectedSlotsTable; + + public DataGridSelectedItemsCollection(DataGrid owningGrid) + { + this.OwningGrid = owningGrid; + _oldSelectedItemsCache = new List(); + _oldSelectedSlotsTable = new IndexToValueTable(); + _selectedItemsCache = new List(); + _selectedSlotsTable = new IndexToValueTable(); + } + + public object this[int index] + { + get + { + if (index < 0 || index >= _selectedSlotsTable.IndexCount) + { + throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _selectedSlotsTable.IndexCount, false); + } + + int slot = _selectedSlotsTable.GetNthIndex(index); + Debug.Assert(slot >= 0, "Expected positive slot."); + return this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(slot)); + } + + set + { + throw new NotSupportedException(); + } + } + + public bool IsFixedSize + { + get + { + return false; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public int Add(object dataItem) + { + if (this.OwningGrid.SelectionMode == DataGridSelectionMode.Single) + { + throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode(); + } + + int itemIndex = this.OwningGrid.DataConnection.IndexOf(dataItem); + if (itemIndex == -1) + { + throw DataGridError.DataGrid.ItemIsNotContainedInTheItemsSource("dataItem"); + } + + Debug.Assert(itemIndex >= 0, "Expected positive itemIndex."); + + int slot = this.OwningGrid.SlotFromRowIndex(itemIndex); + if (_selectedSlotsTable.RangeCount == 0) + { + this.OwningGrid.SelectedItem = dataItem; + } + else + { + this.OwningGrid.SetRowSelection(slot, true /*isSelected*/, false /*setAnchorSlot*/); + } + + return _selectedSlotsTable.IndexOf(slot); + } + + public void Clear() + { + if (this.OwningGrid.SelectionMode == DataGridSelectionMode.Single) + { + throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode(); + } + + if (_selectedSlotsTable.RangeCount > 0) + { + // Clearing the selection does not reset the potential current cell. + if (!this.OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/)) + { + // Edited value couldn't be committed or aborted + return; + } + + this.OwningGrid.ClearRowSelection(true /*resetAnchorSlot*/); + } + } + + public bool Contains(object dataItem) + { + int itemIndex = this.OwningGrid.DataConnection.IndexOf(dataItem); + if (itemIndex == -1) + { + return false; + } + + Debug.Assert(itemIndex >= 0, "Expected positive itemIndex."); + + return ContainsSlot(this.OwningGrid.SlotFromRowIndex(itemIndex)); + } + + public int IndexOf(object dataItem) + { + int itemIndex = this.OwningGrid.DataConnection.IndexOf(dataItem); + if (itemIndex == -1) + { + return -1; + } + + Debug.Assert(itemIndex >= 0, "Expected positive itemIndex."); + + int slot = this.OwningGrid.SlotFromRowIndex(itemIndex); + return _selectedSlotsTable.IndexOf(slot); + } + + public void Insert(int index, object dataItem) + { + throw new NotSupportedException(); + } + + public void Remove(object dataItem) + { + if (this.OwningGrid.SelectionMode == DataGridSelectionMode.Single) + { + throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode(); + } + + int itemIndex = this.OwningGrid.DataConnection.IndexOf(dataItem); + if (itemIndex == -1) + { + return; + } + + Debug.Assert(itemIndex >= 0, "Expected positive itemIndex."); + + if (itemIndex == this.OwningGrid.CurrentSlot && + !this.OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/)) + { + // Edited value couldn't be committed or aborted + return; + } + + this.OwningGrid.SetRowSelection(itemIndex, false /*isSelected*/, false /*setAnchorSlot*/); + } + + public void RemoveAt(int index) + { + if (this.OwningGrid.SelectionMode == DataGridSelectionMode.Single) + { + throw DataGridError.DataGridSelectedItemsCollection.CannotChangeSelectedItemsCollectionInSingleMode(); + } + + if (index < 0 || index >= _selectedSlotsTable.IndexCount) + { + throw DataGridError.DataGrid.ValueMustBeBetween("index", "Index", 0, true, _selectedSlotsTable.IndexCount, false); + } + + int rowIndex = _selectedSlotsTable.GetNthIndex(index); + Debug.Assert(rowIndex > -1, "Expected positive itemIndex."); + + if (rowIndex == this.OwningGrid.CurrentSlot && + !this.OwningGrid.CommitEdit(DataGridEditingUnit.Row, true /*exitEditing*/)) + { + // Edited value couldn't be committed or aborted + return; + } + + this.OwningGrid.SetRowSelection(rowIndex, false /*isSelected*/, false /*setAnchorSlot*/); + } + + public int Count + { + get + { + return _selectedSlotsTable.IndexCount; + } + } + + public bool IsSynchronized + { + get + { + return false; + } + } + + public object SyncRoot + { + get + { + return this; + } + } + + public void CopyTo(Array array, int index) + { + // TODO: Not supported yet. + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + Debug.Assert(this.OwningGrid != null, "Expected non-null owning DataGrid."); + Debug.Assert(this.OwningGrid.DataConnection != null, "Expected non-null owning DataGrid.DataConnection."); + Debug.Assert(_selectedSlotsTable != null, "Expected non-null _selectedSlotsTable."); + + foreach (int slot in _selectedSlotsTable.GetIndexes()) + { + int rowIndex = this.OwningGrid.RowIndexFromSlot(slot); + Debug.Assert(rowIndex > -1, "Expected positive rowIndex."); + yield return this.OwningGrid.DataConnection.GetDataItem(rowIndex); + } + } + + internal DataGrid OwningGrid + { + get; + private set; + } + + internal List SelectedItemsCache + { + get + { + return _selectedItemsCache; + } + + set + { + _selectedItemsCache = value; + UpdateIndexes(); + } + } + + internal void ClearRows() + { + _selectedSlotsTable.Clear(); + _selectedItemsCache.Clear(); + } + + internal bool ContainsSlot(int slot) + { + return _selectedSlotsTable.Contains(slot); + } + + internal bool ContainsAll(int startSlot, int endSlot) + { + int itemSlot = this.OwningGrid.RowGroupHeadersTable.GetNextGap(startSlot - 1); + while (itemSlot <= endSlot) + { + // Skip over the RowGroupHeaderSlots + int nextRowGroupHeaderSlot = this.OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot); + int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endSlot : Math.Min(endSlot, nextRowGroupHeaderSlot - 1); + if (!_selectedSlotsTable.ContainsAll(itemSlot, lastItemSlot)) + { + return false; + } + + itemSlot = this.OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot); + } + + return true; + } + + // Called when an item is deleted from the ItemsSource as opposed to just being unselected + internal void Delete(int slot, object item) + { + if (_oldSelectedSlotsTable.Contains(slot)) + { + this.OwningGrid.SelectionHasChanged = true; + } + + DeleteSlot(slot); + _selectedItemsCache.Remove(item); + } + + internal void DeleteSlot(int slot) + { + _selectedSlotsTable.RemoveIndex(slot); + _oldSelectedSlotsTable.RemoveIndex(slot); + } + + // Returns the inclusive index count between lowerBound and upperBound of all indexes with the given value + internal int GetIndexCount(int lowerBound, int upperBound) + { + return _selectedSlotsTable.GetIndexCount(lowerBound, upperBound, true); + } + + internal IEnumerable GetIndexes() + { + return _selectedSlotsTable.GetIndexes(); + } + + internal IEnumerable GetSlots(int startSlot) + { + return _selectedSlotsTable.GetIndexes(startSlot); + } + + internal SelectionChangedEventArgs GetSelectionChangedEventArgs() + { + List addedSelectedItems = new List(); + List removedSelectedItems = new List(); + + // Compare the old selected indexes with the current selection to determine which items + // have been added and removed since the last time this method was called + foreach (int newSlot in _selectedSlotsTable.GetIndexes()) + { + object newItem = this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(newSlot)); + if (_oldSelectedSlotsTable.Contains(newSlot)) + { + _oldSelectedSlotsTable.RemoveValue(newSlot); + _oldSelectedItemsCache.Remove(newItem); + } + else + { + addedSelectedItems.Add(newItem); + } + } + + foreach (object oldItem in _oldSelectedItemsCache) + { + removedSelectedItems.Add(oldItem); + } + + // The current selection becomes the old selection + _oldSelectedSlotsTable = _selectedSlotsTable.Copy(); + _oldSelectedItemsCache = new List(_selectedItemsCache); + + return new SelectionChangedEventArgs(removedSelectedItems, addedSelectedItems); + } + + internal void InsertIndex(int slot) + { + _selectedSlotsTable.InsertIndex(slot); + _oldSelectedSlotsTable.InsertIndex(slot); + + // It's possible that we're inserting an item that was just removed. If that's the case, + // and the re-inserted item used to be selected, we want to update the _oldSelectedSlotsTable + // to include the item's new index within the collection. + int rowIndex = this.OwningGrid.RowIndexFromSlot(slot); + if (rowIndex != -1) + { + object insertedItem = this.OwningGrid.DataConnection.GetDataItem(rowIndex); + if (insertedItem != null && _oldSelectedItemsCache.Contains(insertedItem)) + { + _oldSelectedSlotsTable.AddValue(slot, true); + } + } + } + + internal void SelectSlot(int slot, bool select) + { + if (this.OwningGrid.RowGroupHeadersTable.Contains(slot)) + { + return; + } + + if (select) + { + if (!_selectedSlotsTable.Contains(slot)) + { + _selectedItemsCache.Add(this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(slot))); + } + + _selectedSlotsTable.AddValue(slot, true); + } + else + { + if (_selectedSlotsTable.Contains(slot)) + { + _selectedItemsCache.Remove(this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(slot))); + } + + _selectedSlotsTable.RemoveValue(slot); + } + } + + internal void SelectSlots(int startSlot, int endSlot, bool select) + { + int itemSlot = this.OwningGrid.RowGroupHeadersTable.GetNextGap(startSlot - 1); + int endItemSlot = this.OwningGrid.RowGroupHeadersTable.GetPreviousGap(endSlot + 1); + + if (select) + { + while (itemSlot <= endItemSlot) + { + // Add the newly selected item slots by skipping over the RowGroupHeaderSlots + int nextRowGroupHeaderSlot = this.OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot); + int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endItemSlot : Math.Min(endItemSlot, nextRowGroupHeaderSlot - 1); + + for (int slot = itemSlot; slot <= lastItemSlot; slot++) + { + if (!_selectedSlotsTable.Contains(slot)) + { + _selectedItemsCache.Add(this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(slot))); + } + } + + _selectedSlotsTable.AddValues(itemSlot, lastItemSlot - itemSlot + 1, true); + itemSlot = this.OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot); + } + } + else + { + while (itemSlot <= endItemSlot) + { + // Remove the unselected item slots by skipping over the RowGroupHeaderSlots + int nextRowGroupHeaderSlot = this.OwningGrid.RowGroupHeadersTable.GetNextIndex(itemSlot); + int lastItemSlot = nextRowGroupHeaderSlot == -1 ? endItemSlot : Math.Min(endItemSlot, nextRowGroupHeaderSlot - 1); + + for (int slot = itemSlot; slot <= lastItemSlot; slot++) + { + if (_selectedSlotsTable.Contains(slot)) + { + _selectedItemsCache.Remove(this.OwningGrid.DataConnection.GetDataItem(this.OwningGrid.RowIndexFromSlot(slot))); + } + } + + _selectedSlotsTable.RemoveValues(itemSlot, lastItemSlot - itemSlot + 1); + itemSlot = this.OwningGrid.RowGroupHeadersTable.GetNextGap(lastItemSlot); + } + } + } + + internal void UpdateIndexes() + { + _oldSelectedSlotsTable.Clear(); + _selectedSlotsTable.Clear(); + + if (this.OwningGrid.DataConnection.DataSource == null) + { + if (this.SelectedItemsCache.Count > 0) + { + this.OwningGrid.SelectionHasChanged = true; + this.SelectedItemsCache.Clear(); + } + } + else + { + List tempSelectedItemsCache = new List(); + foreach (object item in _selectedItemsCache) + { + int index = this.OwningGrid.DataConnection.IndexOf(item); + if (index != -1) + { + tempSelectedItemsCache.Add(item); + _selectedSlotsTable.AddValue(this.OwningGrid.SlotFromRowIndex(index), true); + } + } + + foreach (object item in _oldSelectedItemsCache) + { + int index = this.OwningGrid.DataConnection.IndexOf(item); + if (index == -1) + { + this.OwningGrid.SelectionHasChanged = true; + } + else + { + _oldSelectedSlotsTable.AddValue(this.OwningGrid.SlotFromRowIndex(index), true); + } + } + + _selectedItemsCache = tempSelectedItemsCache; + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTemplateColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTemplateColumn.cs new file mode 100644 index 0000000..5a9a7e5 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTemplateColumn.cs @@ -0,0 +1,161 @@ +// 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 Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column that hosts template-specified + /// content in its cells. + /// + public class DataGridTemplateColumn : DataGridColumn + { + private DataTemplate _cellTemplate; + private DataTemplate _cellEditingTemplate; + + /// + /// Initializes a new instance of the class. + /// + public DataGridTemplateColumn() + { + } + + /// + /// Gets or sets the template that is used to display the contents of a cell that is in editing mode. + /// + public DataTemplate CellEditingTemplate + { + get + { + return _cellEditingTemplate; + } + + set + { + if (_cellEditingTemplate != value) + { + this.RemoveEditingElement(); + _cellEditingTemplate = value; + } + } + } + + /// + /// Gets or sets the template that is used to display the contents of a cell that is not in editing mode. + /// + public DataTemplate CellTemplate + { + get + { + return _cellTemplate; + } + + set + { + if (_cellTemplate != value) + { + if (_cellEditingTemplate == null) + { + this.RemoveEditingElement(); + } + + _cellTemplate = value; + } + } + } + + internal bool HasDistinctTemplates + { + get + { + return this.CellTemplate != this.CellEditingTemplate; + } + } + + /// + /// CancelCellEdit + /// + /// The element that the column displays for a cell in editing mode. + /// The previous, unedited value in the cell being edited. + protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue) + { + _ = GenerateEditingElement(null, null); + } + + /// + /// Gets an element defined by the that is bound to the column's property value. + /// + /// A new editing element that is bound to the column's property value. + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// + /// The is null. + /// + protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) + { + if (this.CellEditingTemplate != null) + { + return this.CellEditingTemplate.LoadContent() as FrameworkElement; + } + + if (this.CellTemplate != null) + { + return this.CellTemplate.LoadContent() as FrameworkElement; + } + + if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } + } + + /// + /// Gets an element defined by the that is bound to the column's property value. + /// + /// A new, read-only element that is bound to the column's property value. + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// + /// The is null. + /// + protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) + { + if (this.CellTemplate != null) + { + return this.CellTemplate.LoadContent() as FrameworkElement; + } + + if (this.CellEditingTemplate != null) + { + return this.CellEditingTemplate.LoadContent() as FrameworkElement; + } + + if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) + { + return null; + } + else + { + throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn)); + } + } + + /// + /// Called when a cell in the column enters editing mode. + /// + /// The element that the column displays for a cell in editing mode. + /// Information about the user gesture that is causing a cell to enter editing mode. + /// null in all cases. + protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + return null; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTextColumn.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTextColumn.cs new file mode 100644 index 0000000..1e30911 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridTextColumn.cs @@ -0,0 +1,433 @@ +// 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; +using System.ComponentModel; +using Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals; +using Windows.UI; +using Windows.UI.Text; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Represents a column that hosts textual content in its cells. + /// + [StyleTypedProperty(Property = "ElementStyle", StyleTargetType = typeof(TextBlock))] + [StyleTypedProperty(Property = "EditingElementStyle", StyleTargetType = typeof(TextBox))] + public class DataGridTextColumn : DataGridBoundColumn + { + private const string DATAGRIDTEXTCOLUMN_fontFamilyName = "FontFamily"; + private const string DATAGRIDTEXTCOLUMN_fontSizeName = "FontSize"; + private const string DATAGRIDTEXTCOLUMN_fontStyleName = "FontStyle"; + private const string DATAGRIDTEXTCOLUMN_fontWeightName = "FontWeight"; + private const string DATAGRIDTEXTCOLUMN_foregroundName = "Foreground"; + private const double DATAGRIDTEXTCOLUMN_leftMargin = 12.0; + private const double DATAGRIDTEXTCOLUMN_rightMargin = 12.0; + + private double? _fontSize; + private FontStyle? _fontStyle; + private FontWeight? _fontWeight; + private Brush _foreground; + + /// + /// Initializes a new instance of the class. + /// + public DataGridTextColumn() + { + this.BindingTarget = TextBox.TextProperty; + } + + /// + /// Gets or sets the font name. + /// + public FontFamily FontFamily + { + get { return (FontFamily)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Identifies the FontFamily dependency property. + /// + public static readonly DependencyProperty FontFamilyProperty = + DependencyProperty.Register( + DATAGRIDTEXTCOLUMN_fontFamilyName, + typeof(FontFamily), + typeof(DataGridTextColumn), + new PropertyMetadata(null, OnFontFamilyPropertyChanged)); + + private static void OnFontFamilyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DataGridTextColumn textColumn = d as DataGridTextColumn; + textColumn.NotifyPropertyChanged(DATAGRIDTEXTCOLUMN_fontFamilyName); + } + + /// + /// Gets or sets the font size. + /// + // Use DefaultValue here so undo in the Designer will set this to NaN + [DefaultValue(double.NaN)] + public double FontSize + { + get + { + return _fontSize ?? double.NaN; + } + + set + { + if (_fontSize != value) + { + _fontSize = value; + NotifyPropertyChanged(DATAGRIDTEXTCOLUMN_fontSizeName); + } + } + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get + { + return _fontStyle ?? FontStyle.Normal; + } + + set + { + if (_fontStyle != value) + { + _fontStyle = value; + NotifyPropertyChanged(DATAGRIDTEXTCOLUMN_fontStyleName); + } + } + } + + /// + /// Gets or sets the font weight or thickness. + /// + public FontWeight FontWeight + { + get + { + return _fontWeight ?? FontWeights.Normal; + } + + set + { + if (!_fontWeight.HasValue || _fontWeight.Value.Weight != value.Weight) + { + _fontWeight = value; + NotifyPropertyChanged(DATAGRIDTEXTCOLUMN_fontWeightName); + } + } + } + + /// + /// Gets or sets a brush that describes the foreground of the column cells. + /// + public Brush Foreground + { + get + { + return _foreground; + } + + set + { + if (_foreground != value) + { + _foreground = value; + NotifyPropertyChanged(DATAGRIDTEXTCOLUMN_foregroundName); + } + } + } + + /// + /// Causes the column cell being edited to revert to the specified value. + /// + /// The element that the column displays for a cell in editing mode. + /// The previous, unedited value in the cell being edited. + protected override void CancelCellEdit(FrameworkElement editingElement, object uneditedValue) + { + TextBox textBox = editingElement as TextBox; + if (textBox != null) + { + string uneditedString = uneditedValue as string; + textBox.Text = uneditedString ?? string.Empty; + } + } + + /// + /// Gets a control that is bound to the column's property value. + /// + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// A new control that is bound to the column's property value. + protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem) + { + TextBox textBox = new TextBox(); + textBox.VerticalAlignment = VerticalAlignment.Stretch; + textBox.Background = new SolidColorBrush(Colors.Transparent); + + if (DependencyProperty.UnsetValue != ReadLocalValue(DataGridTextColumn.FontFamilyProperty)) + { + textBox.FontFamily = this.FontFamily; + } + + if (_fontSize.HasValue) + { + textBox.FontSize = _fontSize.Value; + } + + if (_fontStyle.HasValue) + { + textBox.FontStyle = _fontStyle.Value; + } + + if (_fontWeight.HasValue) + { + textBox.FontWeight = _fontWeight.Value; + } + + RefreshForeground(textBox, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + + if (this.Binding != null) + { + textBox.SetBinding(this.BindingTarget, this.Binding); + } + + return textBox; + } + + /// + /// Gets a read-only element that is bound to the column's property value. + /// + /// The cell that will contain the generated element. + /// The data item represented by the row that contains the intended cell. + /// A new, read-only element that is bound to the column's property value. + protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem) + { + TextBlock textBlockElement = new TextBlock(); + textBlockElement.Margin = new Thickness(DATAGRIDTEXTCOLUMN_leftMargin, 0.0, DATAGRIDTEXTCOLUMN_rightMargin, 0.0); + textBlockElement.VerticalAlignment = VerticalAlignment.Center; + if (DependencyProperty.UnsetValue != ReadLocalValue(DataGridTextColumn.FontFamilyProperty)) + { + textBlockElement.FontFamily = this.FontFamily; + } + + if (_fontSize.HasValue) + { + textBlockElement.FontSize = _fontSize.Value; + } + + if (_fontStyle.HasValue) + { + textBlockElement.FontStyle = _fontStyle.Value; + } + + if (_fontWeight.HasValue) + { + textBlockElement.FontWeight = _fontWeight.Value; + } + + RefreshForeground(textBlockElement, (cell != null & cell.OwningRow != null) ? cell.OwningRow.ComputedForeground : null); + + if (this.Binding != null) + { + textBlockElement.SetBinding(TextBlock.TextProperty, this.Binding); + } + + return textBlockElement; + } + + /// + /// Called when the cell in the column enters editing mode. + /// + /// The element that the column displays for a cell in editing mode. + /// Information about the user gesture that is causing a cell to enter editing mode. + /// The unedited value. + protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs) + { + TextBox textBox = editingElement as TextBox; + if (textBox != null) + { + string uneditedText = textBox.Text; + int len = uneditedText.Length; + KeyRoutedEventArgs keyEventArgs = editingEventArgs as KeyRoutedEventArgs; + if (keyEventArgs != null && keyEventArgs.Key == Windows.System.VirtualKey.F2) + { + // Put caret at the end of the text + textBox.Select(len, len); + } + else + { + // Select all text + textBox.Select(0, len); + } + + return uneditedText; + } + + return string.Empty; + } + + /// + /// Called by the DataGrid control when this column asks for its elements to be updated, because a property changed. + /// + protected internal override void RefreshCellContent(FrameworkElement element, Brush computedRowForeground, string propertyName) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + TextBox textBox = element as TextBox; + if (textBox == null) + { + TextBlock textBlock = element as TextBlock; + if (textBlock == null) + { + throw DataGridError.DataGrid.ValueIsNotAnInstanceOfEitherOr("element", typeof(TextBox), typeof(TextBlock)); + } + + if (propertyName == DATAGRIDTEXTCOLUMN_fontFamilyName) + { + textBlock.FontFamily = this.FontFamily; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontSizeName) + { + SetTextFontSize(textBlock, TextBlock.FontSizeProperty); + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontStyleName) + { + textBlock.FontStyle = this.FontStyle; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontWeightName) + { + textBlock.FontWeight = this.FontWeight; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_foregroundName) + { + RefreshForeground(textBlock, computedRowForeground); + } + else + { + if (this.FontFamily != null) + { + textBlock.FontFamily = this.FontFamily; + } + + SetTextFontSize(textBlock, TextBlock.FontSizeProperty); + textBlock.FontStyle = this.FontStyle; + textBlock.FontWeight = this.FontWeight; + RefreshForeground(textBlock, computedRowForeground); + } + + return; + } + + if (propertyName == DATAGRIDTEXTCOLUMN_fontFamilyName) + { + textBox.FontFamily = this.FontFamily; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontSizeName) + { + SetTextFontSize(textBox, TextBox.FontSizeProperty); + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontStyleName) + { + textBox.FontStyle = this.FontStyle; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_fontWeightName) + { + textBox.FontWeight = this.FontWeight; + } + else if (propertyName == DATAGRIDTEXTCOLUMN_foregroundName) + { + RefreshForeground(textBox, computedRowForeground); + } + else + { + if (this.FontFamily != null) + { + textBox.FontFamily = this.FontFamily; + } + + SetTextFontSize(textBox, TextBox.FontSizeProperty); + textBox.FontStyle = this.FontStyle; + textBox.FontWeight = this.FontWeight; + RefreshForeground(textBox, computedRowForeground); + } + } + + /// + /// Called when the computed foreground of a row changed. + /// + protected internal override void RefreshForeground(FrameworkElement element, Brush computedRowForeground) + { + TextBox textBox = element as TextBox; + if (textBox != null) + { + RefreshForeground(textBox, computedRowForeground); + } + else + { + TextBlock textBlock = element as TextBlock; + if (textBlock != null) + { + RefreshForeground(textBlock, computedRowForeground); + } + } + } + + private void RefreshForeground(TextBlock textBlock, Brush computedRowForeground) + { + if (this.Foreground == null) + { + if (computedRowForeground != null) + { + textBlock.Foreground = computedRowForeground; + } + } + else + { + textBlock.Foreground = this.Foreground; + } + } + + private void RefreshForeground(TextBox textBox, Brush computedRowForeground) + { + if (this.Foreground == null) + { + if (computedRowForeground != null) + { + textBox.Foreground = computedRowForeground; + } + } + else + { + textBox.Foreground = this.Foreground; + } + } + + private void SetTextFontSize(DependencyObject textElement, DependencyProperty fontSizeProperty) + { + double newFontSize = this.FontSize; + if (double.IsNaN(newFontSize)) + { + textElement.ClearValue(fontSizeProperty); + } + else + { + textElement.SetValue(fontSizeProperty, newFontSize); + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridValueConverter.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridValueConverter.cs new file mode 100644 index 0000000..db984e4 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/DataGrid/DataGridValueConverter.cs @@ -0,0 +1,32 @@ +// 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; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.DataGridInternals +{ + internal class DataGridValueConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (targetType != null && TypeHelper.IsNullableType(targetType)) + { + string strValue = value as string; + if (strValue == string.Empty) + { + return null; + } + } + + return value; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj new file mode 100644 index 0000000..0ff8015 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj @@ -0,0 +1,33 @@ + + + uap10.0.17763 + Windows Community Toolkit Controls DataGrid + + This library provides a XAML DataGrid control. It is part of the Windows Community Toolkit. + + + UWP Toolkit Windows Controls XAML DataGrid + Microsoft.Toolkit.Uwp.UI.Controls + preview + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj.DotSettings b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj.DotSettings new file mode 100644 index 0000000..063eb25 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/AssemblyInfo.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..aa5f437 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// 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.Resources; +using System.Runtime.CompilerServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// TODO: Fix tests for WinUI3 +// [assembly: InternalsVisibleTo("UnitTests.UWP")] +[assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Microsoft.Toolkit.UI.Controls.DataGrid.rd.xml b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Microsoft.Toolkit.UI.Controls.DataGrid.rd.xml new file mode 100644 index 0000000..6085355 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Microsoft.Toolkit.UI.Controls.DataGrid.rd.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.Designer.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.Designer.cs new file mode 100644 index 0000000..d625834 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Toolkit.Uwp.UI.Controls.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to row. + /// + internal static string DataGridRowAutomationPeer_ItemType { + get { + return ResourceManager.GetString("DataGridRowAutomationPeer_ItemType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ({0} items). + /// + internal static string DataGridRowGroupHeader_ItemCountPlural { + get { + return ResourceManager.GetString("DataGridRowGroupHeader_ItemCountPlural", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ({0} item). + /// + internal static string DataGridRowGroupHeader_ItemCountSingular { + get { + return ResourceManager.GetString("DataGridRowGroupHeader_ItemCountSingular", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}:. + /// + internal static string DataGridRowGroupHeader_PropertyName { + get { + return ResourceManager.GetString("DataGridRowGroupHeader_PropertyName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Group. + /// + internal static string DefaultRowGroupHeaderPropertyNameAlternative { + get { + return ResourceManager.GetString("DefaultRowGroupHeaderPropertyNameAlternative", resourceCulture); + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.resx b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.resx new file mode 100644 index 0000000..134f466 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Properties/Resources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + row + + + ({0} items) + + + ({0} item) + + + {0}: + + + Group + + \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Themes/Generic.xaml b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Themes/Generic.xaml new file mode 100644 index 0000000..d384a3c --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Themes/Generic.xaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/BindingInfo.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/BindingInfo.cs new file mode 100644 index 0000000..3d4933c --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/BindingInfo.cs @@ -0,0 +1,51 @@ +// 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 Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + /// + /// Stores information about a Binding, including the BindingExpression, BindingTarget and associated Element. + /// + internal class BindingInfo + { + /// + /// Initializes a new instance of the class. + /// + public BindingInfo() + { + } + + /// + /// Initializes a new instance of the class + /// with the specified BindingExpression, DependencyProperty and FrameworkElement. + /// + /// BindingExpression + /// BindingTarget + /// Element + public BindingInfo(BindingExpression bindingExpression, DependencyProperty bindingTarget, FrameworkElement element) + { + this.BindingExpression = bindingExpression; + this.BindingTarget = bindingTarget; + this.Element = element; + } + + /// + /// Gets or sets the BindingExpression. + /// + public BindingExpression BindingExpression { get; set; } + + /// + /// Gets or sets the BindingTarget. + /// + public DependencyProperty BindingTarget { get; set; } + + /// + /// Gets or sets the Element. + /// + public FrameworkElement Element { get; set; } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/DoubleUtil.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/DoubleUtil.cs new file mode 100644 index 0000000..c446dd2 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/DoubleUtil.cs @@ -0,0 +1,135 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + internal static class DoubleUtil + { + internal const double DBL_EPSILON = 1.1102230246251567e-016; + + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. Note that this epsilon is proportional + /// to the numbers themselves to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the AreClose comparison. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + // in case they are Infinities (then epsilon check does not work) + if (value1 == value2) + { + return true; + } + + // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON; + double delta = value1 - value2; + return -eps < delta && eps > delta; + } + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: This method should be used for optimizations only. + /// + /// + /// bool - the result of the GreaterThan comparison. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return value1 > value2 && !AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. Note that this epsilon is proportional to the numbers + /// themselves to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: This method should be used for optimizations only. + /// + /// + /// bool - the result of the GreaterThanOrClose comparison. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThanOrClose(double value1, double value2) + { + return value1 > value2 || AreClose(value1, value2); + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// + /// bool - the result of the IsZero comparison. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * DBL_EPSILON; + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: This method should be used for optimizations only. + /// + /// + /// bool - the result of the LessThan comparison. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return value1 < value2 && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. Note that this epsilon is proportional to the numbers + /// themselves to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: This method should be used for optimizations only. + /// + /// + /// bool - the result of the LessThanOrClose comparison. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return value1 < value2 || AreClose(value1, value2); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Extensions.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Extensions.cs new file mode 100644 index 0000000..7931adf --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Extensions.cs @@ -0,0 +1,255 @@ +// 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; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using Microsoft.Toolkit.Uwp.Utilities; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI.Utilities +{ + internal static class Extensions + { + private static Dictionary> _suspendedHandlers = new Dictionary>(); + + public static bool IsHandlerSuspended(this DependencyObject dependencyObject, DependencyProperty dependencyProperty) + { + return _suspendedHandlers.ContainsKey(dependencyObject) ? _suspendedHandlers[dependencyObject].ContainsKey(dependencyProperty) : false; + } + + /// + /// Walks the visual tree to determine if a particular child is contained within a parent DependencyObject. + /// + /// Parent DependencyObject + /// Child DependencyObject + /// True if the parent element contains the child + internal static bool ContainsChild(this DependencyObject element, DependencyObject child) + { + if (element != null) + { + while (child != null) + { + if (child == element) + { + return true; + } + + // Walk up the visual tree. If the root is hit, try using the framework element's + // parent. This is done because Popups behave differently with respect to the visual tree, + // and it could have a parent even if the VisualTreeHelper doesn't find it. + DependencyObject parent = VisualTreeHelper.GetParent(child); + if (parent == null) + { + FrameworkElement childElement = child as FrameworkElement; + if (childElement != null) + { + parent = childElement.Parent; + } + } + + child = parent; + } + } + + return false; + } + + /// + /// Walks the visual tree to determine if the currently focused element is contained within + /// a parent DependencyObject. The FocusManager's GetFocusedElement method is used to determine + /// the currently focused element, which is updated synchronously. + /// + /// Parent DependencyObject + /// Parent UIElement. Used to query the element's XamlRoot. + /// True if the currently focused element is within the visual tree of the parent + internal static bool ContainsFocusedElement(this DependencyObject element, UIElement uiElement) + { + return (element == null) ? false : element.ContainsChild(GetFocusedElement(uiElement) as DependencyObject); + } + + private static object GetFocusedElement(UIElement uiElement) + { + if (TypeHelper.IsXamlRootAvailable && uiElement.XamlRoot != null) + { + return FocusManager.GetFocusedElement(uiElement.XamlRoot); + } + else + { + return FocusManager.GetFocusedElement(); + } + } + + /// + /// Checks a MemberInfo object (e.g. a Type or PropertyInfo) for the ReadOnly attribute + /// and returns the value of IsReadOnly if it exists. + /// + /// MemberInfo to check + /// true if MemberInfo is read-only, false otherwise + internal static bool GetIsReadOnly(this MemberInfo memberInfo) + { + if (memberInfo != null) + { + // Check if ReadOnlyAttribute is defined on the member + object[] attributes = memberInfo.GetCustomAttributes(typeof(ReadOnlyAttribute), true); + if (attributes != null && attributes.Length > 0) + { + ReadOnlyAttribute readOnlyAttribute = attributes[0] as ReadOnlyAttribute; + Debug.Assert(readOnlyAttribute != null, "Expected non-null readOnlyAttribute."); + return readOnlyAttribute.IsReadOnly; + } + } + + return false; + } + + internal static Type GetItemType(this IEnumerable list) + { + Type listType = list.GetType(); + Type itemType = null; + bool isICustomTypeProvider = false; + + // If it's a generic enumerable, get the generic type. + + // Unfortunately, if data source is fed from a bare IEnumerable, TypeHelper will report an element type of object, + // which is not particularly interesting. It is dealt with it further on. + if (listType.IsEnumerableType()) + { + itemType = listType.GetEnumerableItemType(); + if (itemType != null) + { + isICustomTypeProvider = typeof(ICustomTypeProvider).IsAssignableFrom(itemType); + } + } + + // Bare IEnumerables mean that result type will be object. In that case, try to get something more interesting. + // Or, if the itemType implements ICustomTypeProvider, try to retrieve the custom type from one of the object instances. + if (itemType == null || itemType == typeof(object) || isICustomTypeProvider) + { + // No type was located yet. Does the list have anything in it? + Type firstItemType = null; + IEnumerator en = list.GetEnumerator(); + if (en.MoveNext() && en.Current != null) + { + firstItemType = en.Current.GetCustomOrCLRType(); + } + else + { + firstItemType = list + .Cast() // cast to convert IEnumerable to IEnumerable + .Select(x => x.GetType()) // get the type + .FirstOrDefault(); // get only the first thing to come out of the sequence, or null if empty + } + + if (firstItemType != typeof(object)) + { + return firstItemType; + } + } + + // Couldn't get the CustomType because there were no items. + if (isICustomTypeProvider) + { + return null; + } + + return itemType; + } + + public static void SetStyleWithType(this FrameworkElement element, Style style) + { + if (element.Style != style && (style == null || style.TargetType != null)) + { + element.Style = style; + } + } + + public static void SetValueNoCallback(this DependencyObject obj, DependencyProperty property, object value) + { + obj.SuspendHandler(property, true); + try + { + obj.SetValue(property, value); + } + finally + { + obj.SuspendHandler(property, false); + } + } + + internal static Point Translate(this UIElement fromElement, UIElement toElement, Point fromPoint) + { + if (fromElement == toElement) + { + return fromPoint; + } + else + { + return fromElement.TransformToVisual(toElement).TransformPoint(fromPoint); + } + } + + // If the parent element goes into a background tab, the elements need to be remeasured + // or they will report 0 height. + internal static UIElement EnsureMeasured(this UIElement element) + { + if (element.DesiredSize.Height == 0) + { + element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + return element; + } + + private static void SuspendHandler(this DependencyObject obj, DependencyProperty dependencyProperty, bool incrementSuspensionCount) + { + if (_suspendedHandlers.ContainsKey(obj)) + { + Dictionary suspensions = _suspendedHandlers[obj]; + + if (incrementSuspensionCount) + { + if (suspensions.ContainsKey(dependencyProperty)) + { + suspensions[dependencyProperty]++; + } + else + { + suspensions[dependencyProperty] = 1; + } + } + else + { + Debug.Assert(suspensions.ContainsKey(dependencyProperty), "Expected existing key for dependencyProperty."); + if (suspensions[dependencyProperty] == 1) + { + suspensions.Remove(dependencyProperty); + } + else + { + suspensions[dependencyProperty]--; + } + + if (suspensions.Count == 0) + { + _suspendedHandlers.Remove(obj); + } + } + } + else + { + Debug.Assert(incrementSuspensionCount, "Expected incrementSuspensionCount==true."); + _suspendedHandlers[obj] = new Dictionary(); + _suspendedHandlers[obj][dependencyProperty] = 1; + } + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/IndexToValueTable.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/IndexToValueTable.cs new file mode 100644 index 0000000..c480f90 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/IndexToValueTable.cs @@ -0,0 +1,907 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + internal class IndexToValueTable : IEnumerable> + { + private List> _list; + + public IndexToValueTable() + { + _list = new List>(); + } + + // Begin Public Properties + + /// + /// Gets the total number of indices represented in the table + /// + public int IndexCount + { + get + { + int indexCount = 0; + foreach (Range range in _list) + { + indexCount += range.Count; + } + + return indexCount; + } + } + + /// + /// Gets a value indicating whether the table is empty + /// + public bool IsEmpty + { + get + { + return _list.Count == 0; + } + } + + /// + /// Gets the number of index ranges in the table + /// + public int RangeCount + { + get + { + return _list.Count; + } + } + + // End Public Properties + + // Begin Public methods + + /// + /// Add a value with an associated index to the table + /// + /// Index where the value is to be added or updated + /// Value to add + public void AddValue(int index, T value) + { + AddValues(index, 1, value); + } + + /// + /// Add multiples values with an associated start index to the table + /// + /// index where first value is added + /// Total number of values to add (must be greater than 0) + /// Value to add + public void AddValues(int startIndex, int count, T value) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + AddValuesPrivate(startIndex, count, value, null); + } + + /// + /// Clears the index table + /// + public void Clear() + { + _list.Clear(); + } + + /// + /// Returns true if the given index is contained in the table + /// + /// index to search for + /// True if the index is contained in the table + public bool Contains(int index) + { + return IsCorrectRangeIndex(this.FindRangeIndex(index), index); + } + + /// + /// Returns true if the entire given index range is contained in the table + /// + /// beginning of the range + /// end of the range + /// True if the entire index range is present in the table + public bool ContainsAll(int startIndex, int endIndex) + { + int start = -1; + int end = -1; + + foreach (Range range in _list) + { + if (start == -1 && range.UpperBound >= startIndex) + { + if (startIndex < range.LowerBound) + { + return false; + } + + start = startIndex; + end = range.UpperBound; + if (end >= endIndex) + { + return true; + } + } + else if (start != -1) + { + if (range.LowerBound > end + 1) + { + return false; + } + + end = range.UpperBound; + if (end >= endIndex) + { + return true; + } + } + } + + return false; + } + + /// + /// Returns true if the given index is contained in the table with the the given value + /// + /// index to search for + /// value expected + /// true if the given index is contained in the table with the given value + public bool ContainsIndexAndValue(int index, T value) + { + int lowerRangeIndex = this.FindRangeIndex(index); + return IsCorrectRangeIndex(lowerRangeIndex, index) && _list[lowerRangeIndex].ContainsValue(value); + } + + /// + /// Returns a copy of this IndexToValueTable + /// + /// copy of this IndexToValueTable + public IndexToValueTable Copy() + { + IndexToValueTable copy = new IndexToValueTable(); + foreach (Range range in _list) + { + copy._list.Add(range.Copy()); + } + + return copy; + } + + public int GetNextGap(int index) + { + int targetIndex = index + 1; + int rangeIndex = FindRangeIndex(targetIndex); + if (IsCorrectRangeIndex(rangeIndex, targetIndex)) + { + while (rangeIndex < _list.Count - 1 && _list[rangeIndex].UpperBound == _list[rangeIndex + 1].LowerBound - 1) + { + rangeIndex++; + } + + return _list[rangeIndex].UpperBound + 1; + } + else + { + return targetIndex; + } + } + + public int GetNextIndex(int index) + { + int targetIndex = index + 1; + int rangeIndex = FindRangeIndex(targetIndex); + if (IsCorrectRangeIndex(rangeIndex, targetIndex)) + { + return targetIndex; + } + else + { + rangeIndex++; + return rangeIndex < _list.Count ? _list[rangeIndex].LowerBound : -1; + } + } + + public int GetPreviousGap(int index) + { + int targetIndex = index - 1; + int rangeIndex = FindRangeIndex(targetIndex); + if (IsCorrectRangeIndex(rangeIndex, targetIndex)) + { + while (rangeIndex > 0 && _list[rangeIndex].LowerBound == _list[rangeIndex - 1].UpperBound + 1) + { + rangeIndex--; + } + + return _list[rangeIndex].LowerBound - 1; + } + else + { + return targetIndex; + } + } + + public int GetPreviousIndex(int index) + { + int targetIndex = index - 1; + int rangeIndex = FindRangeIndex(targetIndex); + if (IsCorrectRangeIndex(rangeIndex, targetIndex)) + { + return targetIndex; + } + else + { + return rangeIndex >= 0 && rangeIndex < _list.Count ? _list[rangeIndex].UpperBound : -1; + } + } + + /// + /// Returns the inclusive index count between lowerBound and upperBound of all indexes with the given value + /// + /// lowerBound criteria + /// upperBound criteria + /// value to look for + /// Number of indexes contained in the table between lowerBound and upperBound (inclusive) + public int GetIndexCount(int lowerBound, int upperBound, T value) + { + Debug.Assert(upperBound >= lowerBound, "Expected upperBound greater or equal to lowerBound."); + + if (_list.Count == 0) + { + return 0; + } + + int count = 0; + int index = FindRangeIndex(lowerBound); + if (IsCorrectRangeIndex(index, lowerBound) && _list[index].ContainsValue(value)) + { + count += _list[index].UpperBound - lowerBound + 1; + } + + index++; + while (index < _list.Count && _list[index].UpperBound <= upperBound) + { + if (_list[index].ContainsValue(value)) + { + count += _list[index].Count; + } + + index++; + } + + if (index < _list.Count && IsCorrectRangeIndex(index, upperBound) && _list[index].ContainsValue(value)) + { + count += upperBound - _list[index].LowerBound; + } + + return count; + } + + /// + /// Returns the inclusive index count between lowerBound and upperBound + /// + /// lowerBound criteria + /// upperBound criteria + /// Number of indexes contained in the table between lowerBound and upperBound (inclusive) + public int GetIndexCount(int lowerBound, int upperBound) + { + if (upperBound < lowerBound || _list.Count == 0) + { + return 0; + } + + int count = 0; + int index = this.FindRangeIndex(lowerBound); + if (IsCorrectRangeIndex(index, lowerBound)) + { + count += _list[index].UpperBound - lowerBound + 1; + } + + index++; + while (index < _list.Count && _list[index].UpperBound <= upperBound) + { + count += _list[index].Count; + index++; + } + + if (index < _list.Count && IsCorrectRangeIndex(index, upperBound)) + { + count += upperBound - _list[index].LowerBound; + } + + return count; + } + + /// + /// Returns the number of indexes in this table after a given startingIndex but before + /// reaching a gap of indexes of a given size + /// + /// Index to start at + /// Size of index gap + /// the number of indexes in this table after a given startingIndex but before + /// reaching a gap of indexes of a given size + public int GetIndexCountBeforeGap(int startingIndex, int gapSize) + { + if (_list.Count == 0) + { + return 0; + } + + int count = 0; + int currentIndex = startingIndex; + int rangeIndex = 0; + int gap = 0; + while (gap <= gapSize && rangeIndex < _list.Count) + { + gap += _list[rangeIndex].LowerBound - currentIndex; + if (gap <= gapSize) + { + count += _list[rangeIndex].UpperBound - _list[rangeIndex].LowerBound + 1; + currentIndex = _list[rangeIndex].UpperBound + 1; + rangeIndex++; + } + } + + return count; + } + + /// + /// Returns an enumerator that goes through the indexes present in the table + /// + /// an enumerator that enumerates the indexes present in the table + public IEnumerable GetIndexes() + { + Debug.Assert(_list != null, "Expected non-null _list."); + + foreach (Range range in _list) + { + for (int i = range.LowerBound; i <= range.UpperBound; i++) + { + yield return i; + } + } + } + + /// + /// Returns all the indexes on or after a starting index + /// + /// start index + /// all the indexes on or after a starting index + public IEnumerable GetIndexes(int startIndex) + { + Debug.Assert(_list != null, "Expected non-null _list."); + + int rangeIndex = FindRangeIndex(startIndex); + if (rangeIndex == -1) + { + rangeIndex++; + } + + while (rangeIndex < _list.Count) + { + for (int i = _list[rangeIndex].LowerBound; i <= _list[rangeIndex].UpperBound; i++) + { + if (i >= startIndex) + { + yield return i; + } + } + + rangeIndex++; + } + } + + /// + /// Return the index of the Nth element in the table + /// + /// n + /// the index of the Nth element in the table + public int GetNthIndex(int n) + { + Debug.Assert(n >= 0 && n < this.IndexCount, "Expected n between 0 and IndexCount-1, inclusive."); + + int cumulatedEntries = 0; + foreach (Range range in _list) + { + if (cumulatedEntries + range.Count > n) + { + return range.LowerBound + n - cumulatedEntries; + } + else + { + cumulatedEntries += range.Count; + } + } + + return -1; + } + + /// + /// Returns the value at a given index or the default value if the index is not in the table + /// + /// index to search for + /// the value at the given index or the default value if index is not in the table + public T GetValueAt(int index) => GetValueAt(index, out _); + + /// + /// Returns the value at a given index or the default value if the index is not in the table + /// + /// index to search for + /// set to true by the method if the index was found; otherwise, false + /// the value at the given index or the default value if index is not in the table + public T GetValueAt(int index, out bool found) + { + int rangeIndex = this.FindRangeIndex(index); + if (this.IsCorrectRangeIndex(rangeIndex, index)) + { + found = true; + return _list[rangeIndex].Value; + } + else + { + found = false; + return default(T); + } + } + + /// + /// Returns an index's index within this table + /// + /// index to search for + /// an index's index within this table + public int IndexOf(int index) + { + int cumulatedIndexes = 0; + foreach (Range range in _list) + { + if (range.UpperBound >= index) + { + cumulatedIndexes += index - range.LowerBound; + break; + } + else + { + cumulatedIndexes += range.Count; + } + } + + return cumulatedIndexes; + } + + /// + /// Inserts an index at the given location. This does not alter values in the table + /// + /// index location to insert an index + public void InsertIndex(int index) + { + InsertIndexes(index, 1); + } + + /// + /// Inserts an index into the table with the given value + /// + /// index to insert + /// value for the index + public void InsertIndexAndValue(int index, T value) + { + InsertIndexesAndValues(index, 1, value); + } + + /// + /// Inserts multiple indexes into the table. This does not alter Values in the table + /// + /// first index to insert + /// total number of indexes to insert + public void InsertIndexes(int startIndex, int count) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + InsertIndexesPrivate(startIndex, count, this.FindRangeIndex(startIndex)); + } + + /// + /// Inserts multiple indexes into the table with the given value + /// + /// Index to insert first value + /// Total number of values to insert (must be greater than 0) + /// Value to insert + public void InsertIndexesAndValues(int startIndex, int count, T value) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + int lowerRangeIndex = this.FindRangeIndex(startIndex); + InsertIndexesPrivate(startIndex, count, lowerRangeIndex); + if ((lowerRangeIndex >= 0) && (_list[lowerRangeIndex].LowerBound > startIndex)) + { + // Because of the insert, the original range no longer contains the startIndex + lowerRangeIndex--; + } + + AddValuesPrivate(startIndex, count, value, lowerRangeIndex); + } + + /// + /// Removes an index from the table. This does not alter Values in the table + /// + /// index to remove + public void RemoveIndex(int index) + { + RemoveIndexes(index, 1); + } + + /// + /// Removes a value and its index from the table + /// + /// index to remove + public void RemoveIndexAndValue(int index) + { + RemoveIndexesAndValues(index, 1); + } + + /// + /// Removes multiple indexes from the table. This does not alter Values in the table + /// + /// first index to remove + /// total number of indexes to remove + public void RemoveIndexes(int startIndex, int count) + { + int lowerRangeIndex = this.FindRangeIndex(startIndex); + if (lowerRangeIndex < 0) + { + lowerRangeIndex = 0; + } + + int i = lowerRangeIndex; + while (i < _list.Count) + { + Range range = _list[i]; + if (range.UpperBound >= startIndex) + { + if (range.LowerBound >= startIndex + count) + { + // Both bounds will remain after the removal + range.LowerBound -= count; + range.UpperBound -= count; + } + else + { + int currentIndex = i; + if (range.LowerBound <= startIndex) + { + // Range gets split up + if (range.UpperBound >= startIndex + count) + { + i++; + _list.Insert(i, new Range(startIndex, range.UpperBound - count, range.Value)); + } + + range.UpperBound = startIndex - 1; + } + else + { + range.LowerBound = startIndex; + range.UpperBound -= count; + } + + if (RemoveRangeIfInvalid(range, currentIndex)) + { + i--; + } + } + } + + i++; + } + + if (!this.Merge(lowerRangeIndex)) + { + this.Merge(lowerRangeIndex + 1); + } + } + + /// + /// Removes multiple values and their indexes from the table + /// + /// first index to remove + /// total number of indexes to remove + public void RemoveIndexesAndValues(int startIndex, int count) + { + RemoveValues(startIndex, count); + RemoveIndexes(startIndex, count); + } + + /// + /// Removes a value from the table at the given index. This does not alter other indexes in the table + /// + /// index where value should be removed + public void RemoveValue(int index) + { + RemoveValues(index, 1); + } + + /// + /// Removes multiple values from the table. This does not alter other indexes in the table + /// + /// first index where values should be removed + /// total number of values to remove + public void RemoveValues(int startIndex, int count) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + int lowerRangeIndex = this.FindRangeIndex(startIndex); + if (lowerRangeIndex < 0) + { + lowerRangeIndex = 0; + } + + while ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound < startIndex)) + { + lowerRangeIndex++; + } + + if (lowerRangeIndex >= _list.Count || _list[lowerRangeIndex].LowerBound > startIndex + count - 1) + { + // If all the values are above our below our values, we have nothing to remove + return; + } + + if (_list[lowerRangeIndex].LowerBound < startIndex) + { + // Need to split this up + _list.Insert(lowerRangeIndex, new Range(_list[lowerRangeIndex].LowerBound, startIndex - 1, _list[lowerRangeIndex].Value)); + lowerRangeIndex++; + } + + _list[lowerRangeIndex].LowerBound = startIndex + count; + if (!RemoveRangeIfInvalid(_list[lowerRangeIndex], lowerRangeIndex)) + { + lowerRangeIndex++; + } + + while ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound < startIndex + count)) + { + _list.RemoveAt(lowerRangeIndex); + } + + if ((lowerRangeIndex < _list.Count) && (_list[lowerRangeIndex].UpperBound >= startIndex + count) && + (_list[lowerRangeIndex].LowerBound < startIndex + count)) + { + // Chop off the start of the remaining Range if it contains values that we're removing + _list[lowerRangeIndex].LowerBound = startIndex + count; + RemoveRangeIfInvalid(_list[lowerRangeIndex], lowerRangeIndex); + } + } + + // End Public Methods + + // Begin Private Methods + private void AddValuesPrivate(int startIndex, int count, T value, int? startRangeIndex) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + int endIndex = startIndex + count - 1; + Range newRange = new Range(startIndex, endIndex, value); + if (_list.Count == 0) + { + _list.Add(newRange); + } + else + { + int lowerRangeIndex = startRangeIndex ?? this.FindRangeIndex(startIndex); + Range lowerRange = (lowerRangeIndex < 0) ? null : _list[lowerRangeIndex]; + if (lowerRange == null) + { + if (lowerRangeIndex < 0) + { + lowerRangeIndex = 0; + } + + _list.Insert(lowerRangeIndex, newRange); + } + else + { + if (!lowerRange.Value.Equals(value) && (lowerRange.UpperBound >= startIndex)) + { + // Split up the range + if (lowerRange.UpperBound > endIndex) + { + _list.Insert(lowerRangeIndex + 1, new Range(endIndex + 1, lowerRange.UpperBound, lowerRange.Value)); + } + + lowerRange.UpperBound = startIndex - 1; + if (!RemoveRangeIfInvalid(lowerRange, lowerRangeIndex)) + { + lowerRangeIndex++; + } + + _list.Insert(lowerRangeIndex, newRange); + } + else + { + _list.Insert(lowerRangeIndex + 1, newRange); + if (!Merge(lowerRangeIndex)) + { + lowerRangeIndex++; + } + } + } + + // At this point the newRange has been inserted in the correct place, now we need to remove + // any subsequent ranges that no longer make sense and possibly update the one at newRange.UpperBound + int upperRangeIndex = lowerRangeIndex + 1; + while ((upperRangeIndex < _list.Count) && (_list[upperRangeIndex].UpperBound < endIndex)) + { + _list.RemoveAt(upperRangeIndex); + } + + if (upperRangeIndex < _list.Count) + { + Range upperRange = _list[upperRangeIndex]; + if (upperRange.LowerBound <= endIndex) + { + // Update the range + upperRange.LowerBound = endIndex + 1; + RemoveRangeIfInvalid(upperRange, upperRangeIndex); + } + + Merge(lowerRangeIndex); + } + } + } + + // Returns the index of the range that contains the input or the range before if the input is not found + private int FindRangeIndex(int index) + { + if (_list.Count == 0) + { + return -1; + } + + // Do a binary search for the index + int front = 0; + int end = _list.Count - 1; + Range range; + while (end > front) + { + int median = (front + end) / 2; + range = _list[median]; + if (range.UpperBound < index) + { + front = median + 1; + } + else if (range.LowerBound > index) + { + end = median - 1; + } + else + { + // we found it + return median; + } + } + + if (front == end) + { + range = _list[front]; + if (range.ContainsIndex(index) || (range.UpperBound < index)) + { + // we found it or the index isn't there and we're one range before + return front; + } + else + { + // not found and we're one range after + return front - 1; + } + } + else + { + // end is one index before front in this case so it's the range before + return end; + } + } + + private bool Merge(int lowerRangeIndex) + { + int upperRangeIndex = lowerRangeIndex + 1; + if ((lowerRangeIndex >= 0) && (upperRangeIndex < _list.Count)) + { + Range lowerRange = _list[lowerRangeIndex]; + Range upperRange = _list[upperRangeIndex]; + if (lowerRange.UpperBound + 1 >= upperRange.LowerBound && lowerRange.Value.Equals(upperRange.Value)) + { + lowerRange.UpperBound = Math.Max(lowerRange.UpperBound, upperRange.UpperBound); + _list.RemoveAt(upperRangeIndex); + return true; + } + } + + return false; + } + + private void InsertIndexesPrivate(int startIndex, int count, int lowerRangeIndex) + { + Debug.Assert(count > 0, "Expected a strictly positive count parameter."); + + // Same as AddRange after we fix the indices affected by the insertion + int startRangeIndex = (lowerRangeIndex >= 0) ? lowerRangeIndex : 0; + for (int i = startRangeIndex; i < _list.Count; i++) + { + Range range = _list[i]; + if (range.LowerBound >= startIndex) + { + range.LowerBound += count; + } + else + { + if (range.UpperBound >= startIndex) + { + // Split up this range + i++; + _list.Insert(i, new Range(startIndex, range.UpperBound + count, range.Value)); + range.UpperBound = startIndex - 1; + continue; + } + } + + if (range.UpperBound >= startIndex) + { + range.UpperBound += count; + } + } + } + + private bool IsCorrectRangeIndex(int rangeIndex, int index) + { + return rangeIndex != -1 && _list[rangeIndex].ContainsIndex(index); + } + + private bool RemoveRangeIfInvalid(Range range, int rangeIndex) + { + if (range.UpperBound < range.LowerBound) + { + _list.RemoveAt(rangeIndex); + return true; + } + + return false; + } + + // End Private Methods + + // Begin IEnumerable> Members + public IEnumerator> GetEnumerator() + { + return _list.GetEnumerator(); + } + + // End IEnumerable> Members + + // Begin IEnumerable Members + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); + } + + // End IEnumerable Members +#if DEBUG + + public void PrintIndexes() + { + Debug.WriteLine(this.IndexCount + " indexes"); + foreach (Range range in _list) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0} - {1}", range.LowerBound, range.UpperBound)); + } + } + +#endif + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/KeyboardHelper.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/KeyboardHelper.cs new file mode 100644 index 0000000..72c3c8b --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/KeyboardHelper.cs @@ -0,0 +1,25 @@ +// 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 Windows.System; +using Windows.UI.Core; +using Windows.UI.Xaml; + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + internal static class KeyboardHelper + { + public static void GetMetaKeyState(out bool ctrl, out bool shift) + { + ctrl = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + shift = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); + } + + public static void GetMetaKeyState(out bool ctrl, out bool shift, out bool alt) + { + GetMetaKeyState(out ctrl, out shift); + alt = CoreWindow.GetForCurrentThread().GetKeyState(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Range.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Range.cs new file mode 100644 index 0000000..c36e382 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/Range.cs @@ -0,0 +1,57 @@ +// 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. + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + internal class Range + { + public Range(int lowerBound, int upperBound, T value) + { + LowerBound = lowerBound; + UpperBound = upperBound; + Value = value; + } + + public int Count + { + get + { + return UpperBound - LowerBound + 1; + } + } + + public int LowerBound + { + get; + set; + } + + public int UpperBound + { + get; + set; + } + + public T Value + { + get; + set; + } + + public bool ContainsIndex(int index) + { + return LowerBound <= index && UpperBound >= index; + } + + public bool ContainsValue(object value) + { + return (this.Value == null) ? value == null : this.Value.Equals(value); + } + + public Range Copy() + { + return new Range(LowerBound, UpperBound, Value); + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/TypeHelper.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/TypeHelper.cs new file mode 100644 index 0000000..f66a2ab --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/TypeHelper.cs @@ -0,0 +1,478 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Windows.UI.Xaml.Data; + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + internal static class TypeHelper + { + internal const char LeftIndexerToken = '['; + internal const char PropertyNameSeparator = '.'; + internal const char RightIndexerToken = ']'; + + private static bool isAPIsAvailableInitialized = false; + private static bool isXamlRootAvailable = false; + + // Methods + private static Type FindGenericType(Type definition, Type type) + { + TypeInfo definitionTypeInfo = definition.GetTypeInfo(); + + while (type != null && type != typeof(object)) + { + TypeInfo typeTypeInfo = type.GetTypeInfo(); + + if (typeTypeInfo.IsGenericType && type.GetGenericTypeDefinition() == definition) + { + return type; + } + + if (definitionTypeInfo.IsInterface) + { + foreach (Type type2 in typeTypeInfo.ImplementedInterfaces) + { + Type type3 = FindGenericType(definition, type2); + if (type3 != null) + { + return type3; + } + } + } + + type = typeTypeInfo.BaseType; + } + + return null; + } + + /// + /// Finds an int or string indexer in the specified collection of members, where int indexers take priority + /// over string indexers. If found, this method will return the associated PropertyInfo and set the out index + /// argument to its appropriate value. If not found, the return value will be null, as will the index. + /// + /// Collection of members to search through for an indexer. + /// String value of indexer argument. + /// Resultant index value. + /// Indexer PropertyInfo if found, null otherwise. + private static PropertyInfo FindIndexerInMembers(MemberInfo[] members, string stringIndex, out object[] index) + { + index = null; + ParameterInfo[] parameters; + PropertyInfo stringIndexer = null; + + foreach (PropertyInfo pi in members) + { + if (pi == null) + { + continue; + } + + // Only a single parameter is supported and it must be a string or Int32 value. + parameters = pi.GetIndexParameters(); + if (parameters.Length > 1) + { + continue; + } + + if (parameters[0].ParameterType == typeof(int)) + { + int intIndex = -1; + if (int.TryParse(stringIndex.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out intIndex)) + { + index = new object[] { intIndex }; + return pi; + } + } + + // If string indexer is found save it, in case there is an int indexer. + if (parameters[0].ParameterType == typeof(string)) + { + index = new object[] { stringIndex }; + stringIndexer = pi; + } + } + + return stringIndexer; + } + + /// + /// Gets the default member name that is used for an indexer (e.g. "Item"). + /// + /// Type to check. + /// Default member name. + private static string GetDefaultMemberName(this Type type) + { + DefaultMemberAttribute defaultMemberAttribute = type.GetTypeInfo().GetCustomAttributes().OfType().FirstOrDefault(); + return defaultMemberAttribute == null ? null : defaultMemberAttribute.MemberName; + } + + internal static string GetBindingPropertyName(this Binding binding) + { + return binding?.Path?.Path?.Split('.')?.LastOrDefault(); + } + + /// + /// Finds the PropertyInfo for the specified property path within this Type, and returns + /// the value of GetShortName on its DisplayAttribute, if one exists. GetShortName will return + /// the value of Name if there is no ShortName specified. + /// + /// Type to search + /// property path + /// DisplayAttribute.ShortName if it exists, null otherwise + internal static string GetDisplayName(this Type type, string propertyPath) + { + PropertyInfo propertyInfo = type.GetNestedProperty(propertyPath); + if (propertyInfo != null) + { + DisplayAttribute displayAttribute = propertyInfo.GetCustomAttributes().OfType().FirstOrDefault(); + return displayAttribute == null ? null : displayAttribute.GetShortName(); + } + + return null; + } + + internal static Type GetEnumerableItemType(this Type enumerableType) + { + Type type = FindGenericType(typeof(IEnumerable<>), enumerableType); + if (type != null) + { + return type.GetGenericArguments()[0]; + } + + return enumerableType; + } + + internal static PropertyInfo GetNestedProperty(this Type parentType, string propertyPath) + { + if (parentType != null) + { + object item = null; + return parentType.GetNestedProperty(propertyPath, ref item); + } + + return null; + } + + /// + /// Finds the leaf PropertyInfo for the specified property path, and returns its value + /// if the item is non-null. + /// + /// Type to search. + /// Property path. + /// Parent item which will be set to the property value if non-null. + /// The PropertyInfo. + internal static PropertyInfo GetNestedProperty(this Type parentType, string propertyPath, ref object item) + { + if (parentType == null || string.IsNullOrEmpty(propertyPath)) + { + item = null; + return null; + } + + PropertyInfo propertyInfo = null; + Type propertyType = parentType; + List propertyNames = SplitPropertyPath(propertyPath); + for (int i = 0; i < propertyNames.Count; i++) + { + propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out var index); + if (propertyInfo == null) + { + item = null; + return null; + } + + if (item != null) + { + item = propertyInfo.GetValue(item, index); + } + + propertyType = propertyInfo.PropertyType.GetNonNullableType(); + } + + return propertyInfo; + } + + internal static Type GetNestedPropertyType(this Type parentType, string propertyPath) + { + if (parentType == null || string.IsNullOrEmpty(propertyPath)) + { + return parentType; + } + + PropertyInfo propertyInfo = parentType.GetNestedProperty(propertyPath); + if (propertyInfo != null) + { + return propertyInfo.PropertyType; + } + + return null; + } + + /// + /// Gets the value of a given property path on a particular data item. + /// + /// Parent data item. + /// Property path. + /// Value. + internal static object GetNestedPropertyValue(object item, string propertyPath) + { + if (item != null) + { + Type parentType = item.GetCustomOrCLRType(); + if (string.IsNullOrEmpty(propertyPath)) + { + return item; + } + else if (parentType != null) + { + object nestedValue = item; + parentType.GetNestedProperty(propertyPath, ref nestedValue); + return nestedValue; + } + } + + return null; + } + + internal static Type GetNonNullableType(this Type type) + { + if (IsNullableType(type)) + { + return type.GetGenericArguments()[0]; + } + + return type; + } + + /// + /// Returns the PropertyInfo for the specified property path. If the property path + /// refers to an indexer (e.g. "[abc]"), then the index out parameter will be set to the value + /// specified in the property path. This method only supports indexers with a single parameter + /// that is either an int or a string. Int parameters take priority over string parameters. + /// + /// Type to search. + /// Property path. + /// Set to the index if return value is an indexer, otherwise null. + /// PropertyInfo for either a property or an indexer. + internal static PropertyInfo GetPropertyOrIndexer(this Type type, string propertyPath, out object[] index) + { + index = null; + if (string.IsNullOrEmpty(propertyPath) || propertyPath[0] != LeftIndexerToken) + { + // Return the default value of GetProperty if the first character is not an indexer token. + return type.GetProperty(propertyPath); + } + + if (propertyPath.Length < 2 || propertyPath[propertyPath.Length - 1] != RightIndexerToken) + { + // Return null if the indexer does not meet the standard format (i.e. "[x]"). + return null; + } + + var stringIndex = propertyPath.Substring(1, propertyPath.Length - 2); + var indexer = FindIndexerInMembers(type.GetDefaultMembers(), stringIndex, out index); + if (indexer != null) + { + // We found the indexer, so return it. + return indexer; + } + + if (typeof(System.Collections.IList).IsAssignableFrom(type)) + { + // If the object is of type IList, try to use its default indexer. + indexer = FindIndexerInMembers(typeof(System.Collections.IList).GetDefaultMembers(), stringIndex, out index); + } + + return indexer; + } + + internal static bool IsEnumerableType(this Type enumerableType) + { + return FindGenericType(typeof(IEnumerable<>), enumerableType) != null; + } + + internal static bool IsNullableType(this Type type) + { + return type != null && type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + /* Unused for now + internal static bool IsNullableEnum(this Type type) + { + return type.IsNullableType() && + type.GenericTypeArguments.Length == 1 && + type.GenericTypeArguments[0].GetTypeInfo().IsEnum; + } + */ + + /// + /// If the specified property is an indexer, this method will prepend the object's + /// default member name to it (e.g. "[foo]" returns "Item[foo]"). + /// + /// Declaring data item. + /// Property name. + /// Property with default member name prepended, or property if unchanged. + internal static string PrependDefaultMemberName(object item, string property) + { + if (item != null && !string.IsNullOrEmpty(property) && property[0] == TypeHelper.LeftIndexerToken) + { + // The leaf property name is an indexer, so add the default member name. + Type declaringType = item.GetCustomOrCLRType(); + if (declaringType != null) + { + string defaultMemberName = declaringType.GetNonNullableType().GetDefaultMemberName(); + if (!string.IsNullOrEmpty(defaultMemberName)) + { + return defaultMemberName + property; + } + } + } + + return property; + } + + /// + /// If the specified property is an indexer, this method will remove the object's + /// default member name from it (e.g. "Item[foo]" returns "[foo]"). + /// + /// Property name. + /// Property with default member name removed, or property if unchanged. + internal static string RemoveDefaultMemberName(string property) + { + if (!string.IsNullOrEmpty(property) && property[property.Length - 1] == TypeHelper.RightIndexerToken) + { + // The property is an indexer, so remove the default member name. + int leftIndexerToken = property.IndexOf(TypeHelper.LeftIndexerToken); + if (leftIndexerToken >= 0) + { + return property.Substring(leftIndexerToken); + } + } + + return property; + } + + /// + /// Sets the value of a given property path on a particular item. + /// + /// Parent data item. + /// New child value + /// Property path + internal static void SetNestedPropertyValue(ref object item, object newValue, string propertyPath) + { + if (string.IsNullOrEmpty(propertyPath)) + { + item = newValue; + } + else + { + var propertyPathParts = SplitPropertyPath(propertyPath); + + if (propertyPathParts.Count == 1) + { + item?.GetType().GetProperty(propertyPath)?.SetValue(item, newValue); + } + else + { + object temporaryItem = item; + object nextToLastItem = null; + + PropertyInfo propertyInfo = null; + + for (var i = 0; i < propertyPathParts.Count; i++) + { + propertyInfo = temporaryItem?.GetType().GetProperty(propertyPathParts[i]); + + if (i == propertyPathParts.Count - 2) + { + nextToLastItem = propertyInfo?.GetValue(temporaryItem); + } + + temporaryItem = propertyInfo?.GetValue(temporaryItem); + } + + propertyInfo?.SetValue(nextToLastItem, newValue); + } + } + } + + /// + /// Returns a list of substrings where each one represents a single property within a nested + /// property path which may include indexers. For example, the string "abc.d[efg][h].ijk" + /// would return the substrings: "abc", "d", "[efg]", "[h]", and "ijk". + /// + /// Path to split. + /// List of property substrings. + internal static List SplitPropertyPath(string propertyPath) + { + List propertyPaths = new List(); + if (!string.IsNullOrEmpty(propertyPath)) + { + int startIndex = 0; + for (int index = 0; index < propertyPath.Length; index++) + { + if (propertyPath[index] == PropertyNameSeparator) + { + propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex)); + startIndex = index + 1; + } + else if (startIndex != index && propertyPath[index] == LeftIndexerToken) + { + propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex)); + startIndex = index; + } + else if (index == propertyPath.Length - 1) + { + propertyPaths.Add(propertyPath.Substring(startIndex)); + } + } + } + + return propertyPaths; + } + + /// + /// Returns instance.GetCustomType() if the instance implements ICustomTypeProvider; otherwise, + /// returns instance.GetType(). + /// + /// Object to return the type of + /// Type of the instance + internal static Type GetCustomOrCLRType(this object instance) + { + ICustomTypeProvider customTypeProvider = instance as ICustomTypeProvider; + if (customTypeProvider != null) + { + return customTypeProvider.GetCustomType() ?? instance.GetType(); + } + + return instance == null ? null : instance.GetType(); + } + + internal static bool IsXamlRootAvailable + { + get + { + if (!isAPIsAvailableInitialized) + { + InitializeAPIsAvailable(); + } + + return isXamlRootAvailable; + } + } + + internal static void InitializeAPIsAvailable() + { + isXamlRootAvailable = Windows.Foundation.Metadata.ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot"); + isAPIsAvailableInitialized = true; + } + } +} diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/UISettingsHelper.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/UISettingsHelper.cs new file mode 100644 index 0000000..329b26a --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/UISettingsHelper.cs @@ -0,0 +1,38 @@ +// 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 Windows.UI.ViewManagement; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Utilities +{ + /// + /// Helper class for accessing UISettings properties. + /// + internal static class UISettingsHelper + { + private static UISettings _uiSettings = null; + + internal static bool AreSettingsEnablingAnimations + { + get + { + if (_uiSettings == null) + { + _uiSettings = new UISettings(); + } + + return _uiSettings.AnimationsEnabled; + } + } + + internal static bool AreSettingsAutoHidingScrollBars + { + get + { + // TODO: Use UISettings public API once available + return true; + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/ValidationUtil.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/ValidationUtil.cs new file mode 100644 index 0000000..3fc74e0 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/ValidationUtil.cs @@ -0,0 +1,302 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Threading; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Shapes; + +namespace Microsoft.Toolkit.Uwp.UI.Data.Utilities +{ + /// + /// Static class with methods to help with validation. + /// + internal static class ValidationUtil + { + /// + /// Adds a new ValidationResult to the collection if an equivalent does not exist. + /// + /// ValidationResults to search through + /// ValidationResult to add + public static void AddIfNew(this ICollection collection, ValidationResult value) + { + if (!collection.ContainsEqualValidationResult(value)) + { + collection.Add(value); + } + } + + /// + /// Performs an action and catches any non-critical exceptions. + /// + /// Action to perform + public static void CatchNonCriticalExceptions(Action action) + { + try + { + action(); + } + catch (Exception exception) + { + if (IsCriticalException(exception)) + { + throw; + } + + // Catch any non-critical exceptions + } + } + + /// + /// Determines whether the collection contains an equivalent ValidationResult + /// + /// ValidationResults to search through + /// ValidationResult to search for + /// True when the collection contains an equivalent ValidationResult. + public static bool ContainsEqualValidationResult(this ICollection collection, ValidationResult target) + { + return collection.FindEqualValidationResult(target) != null; + } + + /// + /// Searches a ValidationResult for the specified target member name. If the target is null + /// or empty, this method will return true if there are no member names at all. + /// + /// ValidationResult to search. + /// Member name to search for. + /// True if found. + public static bool ContainsMemberName(this ValidationResult validationResult, string target) + { + int memberNameCount = 0; + foreach (string memberName in validationResult.MemberNames) + { + if (string.Equals(target, memberName)) + { + return true; + } + + memberNameCount++; + } + + return memberNameCount == 0 && string.IsNullOrEmpty(target); + } + + /// + /// Finds an equivalent ValidationResult if one exists. + /// + /// ValidationResults to search through. + /// ValidationResult to find. + /// Equal ValidationResult if found, null otherwise. + public static ValidationResult FindEqualValidationResult(this ICollection collection, ValidationResult target) + { + foreach (ValidationResult oldValidationResult in collection) + { + if (oldValidationResult.ErrorMessage == target.ErrorMessage) + { + bool movedOld = true; + bool movedTarget = true; + IEnumerator oldEnumerator = oldValidationResult.MemberNames.GetEnumerator(); + IEnumerator targetEnumerator = target.MemberNames.GetEnumerator(); + while (movedOld && movedTarget) + { + movedOld = oldEnumerator.MoveNext(); + movedTarget = targetEnumerator.MoveNext(); + + if (!movedOld && !movedTarget) + { + return oldValidationResult; + } + + if (movedOld != movedTarget || oldEnumerator.Current != targetEnumerator.Current) + { + break; + } + } + } + } + + return null; + } + + /// + /// Searches through all Bindings on the specified element and returns a list of BindingInfo objects + /// for each Binding that matches the specified criteria. + /// + /// FrameworkElement to search + /// Only return Bindings with a context element equal to this object + /// If true, only returns TwoWay Bindings + /// If true, ignores elements not typically used for input + /// If true, searches through the children + /// The Binding search will skip all of these Types + /// List of BindingInfo for every Binding found + public static List GetBindingInfo(this FrameworkElement element, object dataItem, bool twoWayOnly, bool useBlockList, bool searchChildren, params Type[] excludedTypes) + { + List bindingData = new List(); + + if (!searchChildren) + { + if (excludedTypes != null) + { + foreach (Type excludedType in excludedTypes) + { + if (excludedType != null && excludedType.IsInstanceOfType(element)) + { + return bindingData; + } + } + } + + return element.GetBindingInfoOfSingleElement(element.DataContext ?? dataItem, dataItem, twoWayOnly, useBlockList); + } + + Stack children = new Stack(); + Stack dataContexts = new Stack(); + children.Push(element); + dataContexts.Push(element.DataContext ?? dataItem); + + while (children.Count != 0) + { + bool searchChild = true; + DependencyObject child = children.Pop(); + object inheritedDataContext = dataContexts.Pop(); + object dataContext = inheritedDataContext; + + // Skip this particular child element if it is one of the excludedTypes + if (excludedTypes != null) + { + foreach (Type excludedType in excludedTypes) + { + if (excludedType != null && excludedType.IsInstanceOfType(child)) + { + searchChild = false; + break; + } + } + } + + // Add the bindings of the child element and push its children onto the stack of remaining elements to search + if (searchChild) + { + FrameworkElement childElement = child as FrameworkElement; + if (childElement != null) + { + dataContext = childElement.DataContext ?? inheritedDataContext; + bindingData.AddRange(childElement.GetBindingInfoOfSingleElement(inheritedDataContext, dataItem, twoWayOnly, useBlockList)); + } + + int childrenCount = VisualTreeHelper.GetChildrenCount(child); + for (int childIndex = 0; childIndex < childrenCount; childIndex++) + { + children.Push(VisualTreeHelper.GetChild(child, childIndex)); + dataContexts.Push(dataContext); + } + } + } + + return bindingData; + } + + /// + /// Gets a list of the specified FrameworkElement's DependencyProperties. This method will return all + /// DependencyProperties of the element unless 'useBlockList' is true, in which case all bindings on elements + /// that are typically not used as input controls will be ignored. + /// + /// FrameworkElement of interest + /// If true, ignores elements not typically used for input + /// List of DependencyProperties + public static List GetDependencyProperties(this FrameworkElement element, bool useBlockList) + { + List dependencyProperties = new List(); + + bool isBlocklisted = useBlockList && + (element is Panel || element is Button || element is Image || element is ScrollViewer || element is TextBlock || + element is Border || element is Shape || element is ContentPresenter); + + if (!isBlocklisted) + { + Type type = element.GetType(); + FieldInfo[] fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); + foreach (FieldInfo field in fields) + { + if (field.FieldType == typeof(DependencyProperty)) + { + dependencyProperties.Add((DependencyProperty)field.GetValue(null)); + } + } + } + + return dependencyProperties; + } + + /// + /// Determines if the specified exception is un-recoverable. + /// + /// The exception. + /// True if the process cannot be recovered from the exception. + public static bool IsCriticalException(Exception exception) + { + return (exception is OutOfMemoryException) || + (exception is StackOverflowException) || + (exception is AccessViolationException) || + (exception is ThreadAbortException); + } + + /// + /// Gets a list of active bindings on the specified FrameworkElement. Bindings are gathered + /// according to the same conditions BindingGroup uses to find bindings of descendant elements + /// within the visual tree. + /// + /// Root FrameworkElement to search under + /// DomainContext of the element's parent + /// Target DomainContext + /// If true, only returns TwoWay Bindings + /// If true, ignores elements not typically used for input + /// List of active bindings on the specified FrameworkElement. + private static List GetBindingInfoOfSingleElement(this FrameworkElement element, object inheritedDataContext, object dataItem, bool twoWayOnly, bool useBlockList) + { + // Now see which of the possible dependency properties are being used + List bindingData = new List(); + foreach (DependencyProperty bindingTarget in element.GetDependencyProperties(useBlockList)) + { + // We add bindings according to the same conditions as BindingGroups: + // Element.Binding.Mode == TwoWay + // Element.Binding.Source == null + // DataItem == ContextElement.DataContext where: + // If Element is ContentPresenter and TargetProperty is Content, ContextElement = Element.Parent + // Else if TargetProperty is DomainContext, ContextElement = Element.Parent + // Else ContextElement = Element + BindingExpression bindingExpression = element.GetBindingExpression(bindingTarget); + if (bindingExpression != null && + bindingExpression.ParentBinding != null && + (!twoWayOnly || bindingExpression.ParentBinding.Mode == BindingMode.TwoWay) && + bindingExpression.ParentBinding.Source == null) + { + object dataContext; + if (bindingTarget == FrameworkElement.DataContextProperty + || (element is ContentPresenter && bindingTarget == ContentPresenter.ContentProperty)) + { + dataContext = inheritedDataContext; + } + else + { + dataContext = element.DataContext ?? inheritedDataContext; + } + + if (dataItem == dataContext) + { + bindingData.Add(new BindingInfo(bindingExpression, bindingTarget, element)); + } + } + } + + return bindingData; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/VisualStates.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/VisualStates.cs new file mode 100644 index 0000000..cf96f19 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/VisualStates.cs @@ -0,0 +1,290 @@ +// 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.Diagnostics; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.UI.Controls.Utilities +{ + /// + /// Names and helpers for visual states in the control. + /// + internal static class VisualStates + { + // GroupCommon + + /// + /// Normal state + /// + public const string StateNormal = "Normal"; + + /// + /// PointerOver state + /// + public const string StatePointerOver = "PointerOver"; + + /// + /// Pressed state + /// + public const string StatePressed = "Pressed"; + + /// + /// Disabled state + /// + public const string StateDisabled = "Disabled"; + + /// + /// Common state group + /// + public const string GroupCommon = "CommonStates"; + + // GroupExpanded + + /// + /// Expanded state + /// + public const string StateExpanded = "Expanded"; + + /// + /// Collapsed state + /// + public const string StateCollapsed = "Collapsed"; + + /// + /// Empty state + /// + public const string StateEmpty = "Empty"; + + // GroupFocus + + /// + /// Unfocused state + /// + public const string StateUnfocused = "Unfocused"; + + /// + /// Focused state + /// + public const string StateFocused = "Focused"; + + /// + /// Focus state group + /// + public const string GroupFocus = "FocusStates"; + + // GroupSelection + + /// + /// Selected state + /// + public const string StateSelected = "Selected"; + + /// + /// Unselected state + /// + public const string StateUnselected = "Unselected"; + + /// + /// Selection state group + /// + public const string GroupSelection = "SelectionStates"; + + // GroupActive + + /// + /// Active state + /// + public const string StateActive = "Active"; + + /// + /// Inactive state + /// + public const string StateInactive = "Inactive"; + + /// + /// Active state group + /// + public const string GroupActive = "ActiveStates"; + + // GroupCurrent + + /// + /// Regular state + /// + public const string StateRegular = "Regular"; + + /// + /// Current state + /// + public const string StateCurrent = "Current"; + + /// + /// CurrentWithFocus state + /// + public const string StateCurrentWithFocus = "CurrentWithFocus"; + + /// + /// Current state group + /// + public const string GroupCurrent = "CurrentStates"; + + // GroupInteraction + + /// + /// Display state + /// + public const string StateDisplay = "Display"; + + /// + /// Editing state + /// + public const string StateEditing = "Editing"; + + /// + /// Interaction state group + /// + public const string GroupInteraction = "InteractionStates"; + + // GroupSort + + /// + /// Unsorted state + /// + public const string StateUnsorted = "Unsorted"; + + /// + /// Sort Ascending state + /// + public const string StateSortAscending = "SortAscending"; + + /// + /// Sort Descending state + /// + public const string StateSortDescending = "SortDescending"; + + /// + /// Sort state group + /// + public const string GroupSort = "SortStates"; + + // GroupValidation + + /// + /// Invalid state + /// + public const string StateInvalid = "Invalid"; + + /// + /// RowInvalid state + /// + public const string StateRowInvalid = "RowInvalid"; + + /// + /// RowValid state + /// + public const string StateRowValid = "RowValid"; + + /// + /// Valid state + /// + public const string StateValid = "Valid"; + +#if FEATURE_VALIDATION + // RuntimeValidationStates + public const string StateInvalidUnfocused = "InvalidUnfocused"; +#endif + + /// + /// Validation state group + /// + public const string GroupValidation = "ValidationStates"; + + // GroupScrollBarsSeparator + + /// + /// SeparatorExpanded state + /// + public const string StateSeparatorExpanded = "SeparatorExpanded"; + + /// + /// ScrollBarsSeparatorCollapsed state + /// + public const string StateSeparatorCollapsed = "SeparatorCollapsed"; + + /// + /// SeparatorExpandedWithoutAnimation state + /// + public const string StateSeparatorExpandedWithoutAnimation = "SeparatorExpandedWithoutAnimation"; + + /// + /// SeparatorCollapsedWithoutAnimation state + /// + public const string StateSeparatorCollapsedWithoutAnimation = "SeparatorCollapsedWithoutAnimation"; + + /// + /// ScrollBarsSeparator state group + /// + public const string GroupScrollBarsSeparator = "ScrollBarsSeparatorStates"; + + // GroupScrollBars + + /// + /// TouchIndicator state + /// + public const string StateTouchIndicator = "TouchIndicator"; + + /// + /// MouseIndicator state + /// + public const string StateMouseIndicator = "MouseIndicator"; + + /// + /// MouseIndicatorFull state + /// + public const string StateMouseIndicatorFull = "MouseIndicatorFull"; + + /// + /// NoIndicator state + /// + public const string StateNoIndicator = "NoIndicator"; + + /// + /// ScrollBars state group + /// + public const string GroupScrollBars = "ScrollBarsStates"; + + /// + /// Use VisualStateManager to change the visual state of the control. + /// + /// + /// Control whose visual state is being changed. + /// + /// + /// true to use transitions when updating the visual state, false to + /// snap directly to the new visual state. + /// + /// + /// Ordered list of state names and fallback states to transition into. + /// Only the first state to be found will be used. + /// + public static void GoToState(Control control, bool useTransitions, params string[] stateNames) + { + Debug.Assert(control != null, "Expected non-null control."); + + if (stateNames == null) + { + return; + } + + foreach (string name in stateNames) + { + if (VisualStateManager.GoToState(control, name, useTransitions)) + { + break; + } + } + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/WeakEventListener.cs b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/WeakEventListener.cs new file mode 100644 index 0000000..a8a9865 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Utilities/WeakEventListener.cs @@ -0,0 +1,79 @@ +// 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; + +namespace Microsoft.Toolkit.Uwp.Utilities +{ + /// + /// Implements a weak event listener that allows the owner to be garbage + /// collected if its only remaining link is an event handler. + /// Note: Copied from Microsoft.Toolkit.Uwp.Helpers.WeakEventListener to avoid taking a + /// dependency on Microsoft.Toolkit.Uwp.dll and Microsoft.Toolkit.dll. + /// + /// Type of instance listening for the event. + /// Type of source for the event. + /// Type of event arguments for the event. + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + internal sealed class WeakEventListener + where TInstance : class + { + /// + /// WeakReference to the instance listening for the event. + /// + private WeakReference weakInstance; + + /// + /// Initializes a new instance of the class. + /// + /// Instance subscribing to the event. + public WeakEventListener(TInstance instance) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + weakInstance = new WeakReference(instance); + } + + /// + /// Gets or sets the method to call when the event fires. + /// + public Action OnEventAction { get; set; } + + /// + /// Gets or sets the method to call when detaching from the event. + /// + public Action> OnDetachAction { get; set; } + + /// + /// Handler for the subscribed event calls OnEventAction to handle it. + /// + /// Event source. + /// Event arguments. + public void OnEvent(TSource source, TEventArgs eventArgs) + { + if (weakInstance.TryGetTarget(out var target)) + { + // Call registered action + OnEventAction?.Invoke(target, source, eventArgs); + } + else + { + // Detach from event + Detach(); + } + } + + /// + /// Detaches from the subscribed event. + /// + public void Detach() + { + OnDetachAction?.Invoke(this); + OnDetachAction = null; + } + } +} \ No newline at end of file diff --git a/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/VisualStudioToolsManifest.xml b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/VisualStudioToolsManifest.xml new file mode 100644 index 0000000..7619349 --- /dev/null +++ b/Tests/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/VisualStudioToolsManifest.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..c061eef --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,45 @@ +jobs: + - job: Windows + pool: + vmImage: windows-2019 + steps: + - task: gitversion/setup@0 + displayName: Install GitVersion + inputs: + versionSpec: '5.6.3' + + - task: gitversion/execute@0 + displayName: GitVersion + inputs: + useConfigFile: true + configFilePath: gitversion.yml + + - task: DotNetCoreCLI@2 + displayName: Build Uno.WinUI3Convert + inputs: + command: build + arguments: --verbosity detailed --configuration Release "-p:PackageOutputPath=$(Build.ArtifactStagingDirectory)" "-p:PackageVersion=$(GITVERSION.SemVer)" + workingDirectory: src/Uno.WinUI3Convert + + - task: DotNetCoreCLI@2 + displayName: Install Uno.WinUI3Convert + inputs: + command: custom + custom: tool + arguments: install --global --add-source $(Build.ArtifactStagingDirectory) --version $(GITVERSION.SemVer) uno.winui3convert + + - powershell: winui3convert Tests\Microsoft.Toolkit.Uwp.UI.Controls.DataGrid Tests\out\Microsoft.Toolkit.Uwp.UI.Controls.DataGrid + displayName: Convert Microsoft.Toolkit.Uwp.UI.Controls.DataGrid sources + + - task: MSBuild@1 + displayName: Build Microsoft.Toolkit.Uwp.UI.Controls.DataGrid + inputs: + solution: Tests/out/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid/Microsoft.Toolkit.Uwp.UI.Controls.DataGrid.csproj + configuration: Release + msbuildArguments: /ds /r + maximumCpuCount: true + + - task: PublishBuildArtifacts@1 + displayName: Publish Artifacts + inputs: + artifactName: Tool diff --git a/build/uno-logo.png b/build/uno-logo.png new file mode 100644 index 0000000..1aa3f75 Binary files /dev/null and b/build/uno-logo.png differ diff --git a/gitversion.yml b/gitversion.yml new file mode 100644 index 0000000..238e8a8 --- /dev/null +++ b/gitversion.yml @@ -0,0 +1,54 @@ +assembly-versioning-scheme: MajorMinorPatch +mode: Mainline +next-version: 1.0 + +branches: + master: + mode: ContinuousDeployment + regex: master + tag: dev + increment: Minor + is-source-branch-for: ['beta', 'stable'] + + pull-request: + regex: ^(pull|pull\-requests|pr)[/-] + mode: ContinuousDeployment + tag: 'PullRequest' + tag-number-pattern: '[/-](?\d+)[-/]' + increment: Inherit + + beta: + mode: ContinuousDeployment + regex: ^release/beta/.* + tag: beta + increment: none + source-branches: ['master'] + + stable: + regex: ^release/stable/.* + tag: '' + increment: Patch + source-branches: ['master','beta'] + is-mainline: true + + dev: + mode: ContinuousDeployment + regex: ^dev/.*?/(.*?) + tag: dev.{BranchName} + source-branches: ['master', 'release', 'projects', 'feature'] + increment: none + + projects: + tag: proj-{BranchName} + regex: ^projects/(.*?) + source-branches: ['master'] + increment: none + + feature: + tag: feature.{BranchName} + regex: ^feature/(.*?) + source-branches: ['master'] + increment: none + +ignore: + sha: [] \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..15f9ea2 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,24 @@ + + + nventive + Copyright (C) 2020-$([System.DateTime]::Now.ToString(`yyyy`)) nventive inc. - all rights reserved + uno-logo.png + https://github.com/unoplatform/winui3-convert + $(BUILD_REPOSITORY_URI) + + + + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + + + + + + + \ No newline at end of file diff --git a/src/Uno.WinUI3Convert.sln b/src/Uno.WinUI3Convert.sln new file mode 100644 index 0000000..495adc4 --- /dev/null +++ b/src/Uno.WinUI3Convert.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30711.63 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.WinUI3Convert", "Uno.WinUI3Convert\Uno.WinUI3Convert.csproj", "{DB254870-0503-4C62-8985-6D50885A3542}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB254870-0503-4C62-8985-6D50885A3542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB254870-0503-4C62-8985-6D50885A3542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB254870-0503-4C62-8985-6D50885A3542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB254870-0503-4C62-8985-6D50885A3542}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {16F1C62D-11B1-457C-B75C-E6F2105014E1} + EndGlobalSection +EndGlobal diff --git a/src/Uno.WinUI3Convert/ConvertCommand.cs b/src/Uno.WinUI3Convert/ConvertCommand.cs new file mode 100644 index 0000000..217b618 --- /dev/null +++ b/src/Uno.WinUI3Convert/ConvertCommand.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Uno.WinUI3Convert +{ + public class ConvertCommand + { + public static int Execute(DirectoryInfo source, DirectoryInfo destination, bool overwrite, IHost host) + { + var logger = + host.Services + .GetRequiredService() + .CreateLogger(); + + try + { + if (destination.Exists) + { + if (overwrite) + { + destination.Delete(true); + + logger.LogInformation($"Directory \"{destination}\" deleted."); + } + else + { + logger.LogError("Destination exists and overwrite flag is not set."); + + return -1; + } + } + + destination.Create(); + + logger.LogInformation($"Copying files to \"{destination}\"..."); + + CopyFiles(source, destination); + + logger.LogInformation($"Rewriting files..."); + + RewriteFiles(destination, logger); + + logger.LogInformation($"Rewriting projects..."); + + RewriteProjects(destination, logger); + + logger.LogInformation($"Done."); + + return 0; + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred."); + + return -1; + } + } + + private static void CopyFiles(DirectoryInfo source, DirectoryInfo destination) + { + var subdirectories = source.EnumerateDirectories("*", SearchOption.AllDirectories); + + foreach (var directory in subdirectories) + { + var pathFromBase = Path.GetRelativePath(source.FullName, directory.FullName); + + Directory.CreateDirectory(Path.Combine(destination.FullName, pathFromBase)); + } + + var files = source.EnumerateFiles("*", SearchOption.AllDirectories); + + foreach (var file in files) + { + var pathFromBase = Path.GetRelativePath(source.FullName, file.FullName); + + File.Copy(file.FullName, Path.Combine(destination.FullName, pathFromBase)); + } + } + + private static void RewriteFiles(DirectoryInfo source, ILogger logger) + { + var files = source.EnumerateFiles("*.cs", SearchOption.AllDirectories); + + var mappings = new Dictionary() + { + // Discard + { "using Windows.UI.Input;", string.Empty }, + + // Usings + { "using Windows.UI.Text;", "using Microsoft.UI.Text;\r\nusing Windows.UI.Text;" }, + { "using Windows.UI.Xaml.Automation.Peers;", "using Microsoft.UI.Xaml.Automation.Peers;" }, + { "using Windows.UI.Xaml.Automation.Provider;", "using Microsoft.UI.Xaml.Automation.Provider;" }, + { "using Windows.UI.Xaml.Automation;", "using Microsoft.UI.Xaml.Automation;" }, + { "using Windows.UI.Xaml.Controls.Primitives;", "using Microsoft.UI.Xaml.Controls.Primitives;" }, + { "using Windows.UI.Xaml.Controls;", "using Microsoft.UI.Xaml.Controls;" }, + { "using Windows.UI.Xaml.Data;", "using Microsoft.UI.Xaml.Data;" }, + { "using Windows.UI.Xaml.Input;", "using Microsoft.UI.Xaml.Input;" }, + { "using Windows.UI.Xaml.Media.Animation;", "using Microsoft.UI.Xaml.Media.Animation;" }, + { "using Windows.UI.Xaml.Media;", "using Microsoft.UI.Xaml.Media;" }, + { "using Windows.UI.Xaml.Shapes;", "using Microsoft.UI.Xaml.Shapes;" }, + { "using Windows.UI.Xaml;", "using Microsoft.UI.Xaml;" }, + { "using Windows.UI;", "using Microsoft.UI;" }, + + // Namespaces + { "Windows.UI.Xaml.Automation.Peers", "Microsoft.UI.Xaml.Automation.Peers" }, + { "Windows.UI.Xaml.Automation", "Microsoft.UI.Xaml.Automation" }, + { "Windows.UI.Xaml.Controls", "Microsoft.UI.Xaml.Controls" }, + }; + + var regexes = new Dictionary() + { + // Microsoft.System conflict with System + { "(global::)?System\\.Collections", "global::System.Collections" }, + { "(global::)?System\\.ComponentModel", "global::System.ComponentModel" }, + { "(global::)?System\\.Globalization", "global::System.Globalization" }, + { "(global::)?System\\.Reflection", "global::System.Reflection" }, + }; + + foreach (var file in files) + { + logger.LogInformation($"Rewriting {file}"); + + var content = File.ReadAllText(file.FullName); + + foreach (var mapping in mappings) + { + content = content.Replace(mapping.Key, mapping.Value); + } + + foreach (var regex in regexes) + { + content = Regex.Replace(content, regex.Key, regex.Value); + } + + File.WriteAllText(file.FullName, content); + } + } + + private static void RewriteProjects(DirectoryInfo source, ILogger logger) + { + var projects = source.EnumerateFiles("*.csproj", SearchOption.AllDirectories); + + foreach (var project in projects) + { + logger.LogInformation($"Rewriting {project}"); + + var document = XDocument.Load(project.FullName); + + document.Root.Attribute("Sdk").Value = "MSBuild.Sdk.Extras/3.0.22"; + + document.Root.Descendants("TargetFramework").Single().Value = "net5.0-windows10.0.18362.0"; + + var winUIpackageReference = document.Root.Descendants("PackageReference").SingleOrDefault(e => e.Attribute("Include").Value == "Microsoft.WinUI"); + + if (winUIpackageReference != null) + { + winUIpackageReference.Attribute("Version").Value = "3.0.0-preview3.201113.0"; + } + else + { + document.Root.Add(new XElement("ItemGroup", new XElement("PackageReference", new XAttribute("Include", "Microsoft.WinUI"), new XAttribute("Version", "3.0.0-preview3.201113.0"), null))); + } + + using (var xw = XmlWriter.Create(project.FullName, new XmlWriterSettings() { Encoding = Encoding.UTF8, OmitXmlDeclaration = true, Indent = true, NamespaceHandling = NamespaceHandling.OmitDuplicates })) + { + document.Save(xw); + } + } + } + } +} diff --git a/src/Uno.WinUI3Convert/Program.cs b/src/Uno.WinUI3Convert/Program.cs new file mode 100644 index 0000000..ddbded7 --- /dev/null +++ b/src/Uno.WinUI3Convert/Program.cs @@ -0,0 +1,41 @@ +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Hosting; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Uno.WinUI3Convert +{ + class Program + { + static Task Main(string[] args) + { + var command = new RootCommand("Migrate UWP projects to WinUI 3") + { + new Argument("source", "Source directory") + { + Arity = ArgumentArity.ExactlyOne + } + .ExistingOnly(), + + new Argument("destination", "Destination directory") + { + Arity = ArgumentArity.ExactlyOne + }, + + new Option("--overwrite", "Overwrite destination"), + }; + + command.Handler = CommandHandler.Create(ConvertCommand.Execute); + + return new CommandLineBuilder(command) + .UseHost(_ => Host.CreateDefaultBuilder()) + .UseDefaults() + .Build() + .InvokeAsync(args); + } + } +} diff --git a/src/Uno.WinUI3Convert/Uno.WinUI3Convert.csproj b/src/Uno.WinUI3Convert/Uno.WinUI3Convert.csproj new file mode 100644 index 0000000..d7251e4 --- /dev/null +++ b/src/Uno.WinUI3Convert/Uno.WinUI3Convert.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + A dotnet tool to migrate UWP projects to WinUI 3 + true + true + winui3convert + + + + + + +