File: MS\Internal\Documents\DocumentGrid.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// 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.
 
//
// Description: DocumentGrid displays DocumentPaginator content in a grid-like
//              arrangement and is used by DocumentViewer to display documents.
//
 
 
using MS.Internal;
using MS.Internal.Media;
using MS.Utility;
using MS.Win32;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
 
namespace MS.Internal.Documents
{
    /// <summary>
    /// DocumentGrid is an internal Avalon FrameworkElement that executes all the
    /// "heavy lifting" involved in loading and displaying an DocumentPaginator-based
    /// document inside of a DocumentViewer control.
    /// </summary>
    /// <speclink>http://d2/DRX/default.aspx</speclink>
    internal class DocumentGrid : FrameworkElement, IDocumentScrollInfo
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
        #region Constructors
 
        /// <summary>
        /// Static constructor
        /// </summary>
        static DocumentGrid()
        {
            //Register for the RequestBringIntoView event so we can get BIV events for
            //TextEditor IP movements.
            EventManager.RegisterClassHandler(typeof(DocumentGrid),
                RequestBringIntoViewEvent,
                new RequestBringIntoViewEventHandler(OnRequestBringIntoView));
            //Register the default ContextMenu
            DocumentGridContextMenu.RegisterClassHandler();
        }
 
        /// <summary>
        /// The constructor
        /// </summary>
        public DocumentGrid() : base()
        {
            Initialize();
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
        #region Internal Methods
        /// <summary>
        /// Hit-Test on the multi-page UI scope to return a DocumentPage
        /// that contains this point
        /// </summary>
        /// <param name="point">Point in pixel unit, relative to the UI Scope's coordinates</param>
        /// <returns>A DocumentPage that is hit or null if no page is hit</returns>
        internal DocumentPage GetDocumentPageFromPoint(Point point)
        {
            DocumentPageView dp = GetDocumentPageViewFromPoint(point);
 
            // if we hit a DocumentPageView we can return its DocumentPage.
            if (dp != null)
            {
                return dp.DocumentPage;
            }
 
            //Nothing hit, return null.
            return null;
        }
 
        #endregion Internal Methods
 
 
        //------------------------------------------------------
        //
        //  Public Interfaces
        //
        //------------------------------------------------------
        #region Interface Implementations
 
 
        #region IDocumentScrollInfo
        //------------------------------------------------------
        //
        //  IDocumentScrollInfo Methods
        //
        //------------------------------------------------------
 
        /// <summary>
        /// Scroll content by one line to the top.
        /// </summary>
        public void LineUp()
        {
            if (_canVerticallyScroll)
            {
                SetVerticalOffsetInternal(VerticalOffset - _verticalLineScrollAmount);
            }
        }
 
        /// <summary>
        /// Scroll content by one line to the bottom.
        /// </summary>
        public void LineDown()
        {
            if (_canVerticallyScroll)
            {
                SetVerticalOffsetInternal(VerticalOffset + _verticalLineScrollAmount);
 
                //Perf Tracing - Mark LineDown Start
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLineDown);
            }
        }
 
        /// <summary>
        /// Scroll content by one line to the left.
        /// </summary>
        public void LineLeft()
        {
            if (_canHorizontallyScroll)
            {
                SetHorizontalOffsetInternal(HorizontalOffset - _horizontalLineScrollAmount);
            }
        }
 
        /// <summary>
        /// Scroll content by one line to the right.
        /// </summary>
        public void LineRight()
        {
            if (_canHorizontallyScroll)
            {
                SetHorizontalOffsetInternal(HorizontalOffset + _horizontalLineScrollAmount);
            }
        }
 
        /// <summary>
        /// Scroll content by one viewport to the top.
        /// </summary>
        public void PageUp()
        {
            SetVerticalOffsetInternal(VerticalOffset - ViewportHeight);
        }
 
        /// <summary>
        /// Scroll content by one viewport to the bottom.
        /// </summary>
        public void PageDown()
        {
            SetVerticalOffsetInternal(VerticalOffset + ViewportHeight);
 
            //Perf Tracing - Mark PageDown Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageDown, (int)VerticalOffset);
        }
 
        /// <summary>
        /// Scroll content by one viewport to the left.
        /// </summary>
        public void PageLeft()
        {
            SetHorizontalOffsetInternal(HorizontalOffset - ViewportWidth);
        }
 
        /// <summary>
        /// Scroll content by one viewport to the right.
        /// </summary>
        public void PageRight()
        {
            SetHorizontalOffsetInternal(HorizontalOffset + ViewportWidth);
        }
 
        /// <summary>
        /// Scroll content up via the mousewheel.
        /// </summary>
        public void MouseWheelUp()
        {
            if (CanMouseWheelVerticallyScroll)
            {
                SetVerticalOffsetInternal(VerticalOffset - MouseWheelVerticalScrollAmount);
            }
            else
            {
                PageUp();
            }
        }
 
        /// <summary>
        /// Scroll content down via the mousewheel.
        /// </summary>
        public void MouseWheelDown()
        {
            if (CanMouseWheelVerticallyScroll)
            {
                SetVerticalOffsetInternal(VerticalOffset + MouseWheelVerticalScrollAmount);
            }
            else
            {
                PageDown();
            }
        }
 
        /// <summary>
        /// Scroll content left via the mousewheel.
        /// </summary>
        public void MouseWheelLeft()
        {
            if (CanMouseWheelHorizontallyScroll)
            {
                SetHorizontalOffsetInternal(HorizontalOffset - MouseWheelHorizontalScrollAmount);
            }
            else
            {
                PageLeft();
            }
        }
 
        /// <summary>
        /// Scroll content right via the mousewheel.
        /// </summary>
        public void MouseWheelRight()
        {
            if (CanMouseWheelHorizontallyScroll)
            {
                SetHorizontalOffsetInternal(HorizontalOffset + MouseWheelHorizontalScrollAmount);
            }
            else
            {
                PageRight();
            }
        }
 
        /// <summary>
        /// Ensures that the specified visual is made visible.
        /// </summary>
        /// <returns>
        /// A rectangle in the IScrollInfo's coordinate space that has been made visible.
        /// Other ancestors to in turn make this new rectangle visible.
        /// The rectangle should generally be a transformed version of the input rectangle.  In some cases, like
        /// when the input rectangle cannot entirely fit in the viewport, the return value might be smaller.
        /// </returns>
        public Rect MakeVisible(Visual v, Rect r)
        {
            if (Content != null && v != null)
            {
                ContentPosition cp = Content.GetObjectPosition(v);
                MakeContentPositionVisibleAsync(new MakeVisibleData(v, cp, r));
            }
 
            return r;
        }
 
        /// <summary>
        /// Ensures that the specified object is made visible, given that the page it lives on is already known.
        /// </summary>
        /// <returns>
        /// A rectangle in the IScrollInfo's coordinate space that has been made visible.
        /// Other ancestors to in turn make this new rectangle visible.
        /// The rectangle should generally be a transformed version of the input rectangle.  In some cases, like
        /// when the input rectangle cannot entirely fit in the viewport, the return value might be smaller.
        /// </returns>
        public Rect MakeVisible(object o, Rect r, int pageNumber)
        {
            ContentPosition cp = Content.GetObjectPosition(o);
            MakeVisibleAsync(new MakeVisibleData(o as Visual, cp, r), pageNumber);
            return r;
        }
 
        /// <summary>
        /// Scrolls the current selection into view.  Requests for empty or
        /// invalid selections will do nothing.
        /// </summary>
        public void MakeSelectionVisible()
        {
            //We can only continue if we have a TextEditor attached...
            if (TextEditor != null && TextEditor.Selection != null)
            {
                //Get the TextPointer for the start of our selection.
                ITextPointer tp = TextEditor.Selection.Start;
 
                //Ensure that the TextPointer we use has gravity set to forwards (or into
                //the selection) so that the selected text is always displayed.
                tp = tp.CreatePointer(LogicalDirection.Forward);
 
                //If the TextPointer is also a ContentPosition, we can
                //make that ContentPosition visible.
                ContentPosition cp = tp as ContentPosition;
                MakeContentPositionVisibleAsync(new MakeVisibleData(null, cp, Rect.Empty));
            }
        }
 
        /// <summary>
        /// Scrolls the requested page into view.
        /// </summary>
        /// <param name="pageNumber">The page to make visible.</param>
        public void MakePageVisible(int pageNumber)
        {
            //If we're moving more than one page then this is a "page jump"
            //and we should log the perf event.
            if (Math.Abs(pageNumber - _firstVisiblePageNumber) > 1)
            {
                //Perf Tracing - Mark Page Jump Start
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageJump, _firstVisiblePageNumber, pageNumber);
            }
 
            //Clip the offset into range for out-of-range page numbers
            if (pageNumber < 0)
            {
                //Clip to the top-left of the document
                SetVerticalOffsetInternal(0.0d);
                SetHorizontalOffsetInternal(0.0d);
            }
            else if (pageNumber >= _pageCache.PageCount || _rowCache.RowCount == 0)
            {
                //If the doc is done loading, then this page is out of range.
                if (_pageCache.IsPaginationCompleted && _rowCache.HasValidLayout)
                {
                    //Clip to the bottom-right of the document
                    SetVerticalOffsetInternal(ExtentHeight);
                    SetHorizontalOffsetInternal(ExtentWidth);
                }
                else
                {
                    //The doc is not done loading.
                    //Wait for the page to be laid out and try again.
                    _pageJumpAfterLayout = true;
                    _pageJumpAfterLayoutPageNumber = pageNumber;
                }
            }
            else
            {
                //This page is valid, so scroll to it now.
                RowInfo scrolledRow = _rowCache.GetRowForPageNumber(pageNumber);
                SetVerticalOffsetInternal(scrolledRow.VerticalOffset);
 
                //Calculate the Horizontal offset of the page we're bringing into view:
                double horizontalOffset = GetHorizontalOffsetForPage(scrolledRow, pageNumber);
                SetHorizontalOffsetInternal(horizontalOffset);
            }
        }
 
        /// <summary>
        /// Scrolls the next row of pages into view.  This differs from
        /// IScrollInfo?s "PageDown" in that PageDown pages by Viewports
        /// which may not coincide with page dimensions, whereas
        /// ScrollToNextRow takes these dimensions into account so that
        /// precisely the next row of pages is displayed.
        /// </summary>
        public void ScrollToNextRow()
        {
            //We change our vertical offset to be the offset of the next row (if there is one).
            //If there isn't, we do nothing.
            int nextRow = _firstVisibleRow + 1;
 
            if (nextRow < _rowCache.RowCount)
            {
                //Get the next row.
                RowInfo row = _rowCache.GetRow(nextRow);
                SetVerticalOffsetInternal(row.VerticalOffset);
            }
        }
 
        /// Scrolls the previous row of pages into view.  This differs from
        /// IScrollInfo?s "PageUp" in that PageUp pages by Viewports
        /// which may not coincide with page dimensions, whereas
        /// ScrollToPreviousRow takes these dimensions into account so that
        /// precisely the previously row of pages is displayed.
        public void ScrollToPreviousRow()
        {
            //We change our vertical offset to be the offset of the previous row (if there is one).
            int previousRow = _firstVisibleRow - 1;
 
            if (previousRow >= 0 && previousRow < _rowCache.RowCount)
            {
                //Get the previous row.
                RowInfo row = _rowCache.GetRow(previousRow);
                SetVerticalOffsetInternal(row.VerticalOffset);
            }
        }
 
        /// <summary>
        /// Scrolls to the top of the document.
        /// </summary>
        public void ScrollToHome()
        {
            //We just set the VerticalOffset to 0.
            SetVerticalOffsetInternal(0);
        }
 
        /// <summary>
        /// Scrolls to the bottom of the document.
        /// </summary>
        public void ScrollToEnd()
        {
            //We just set the VerticalOffset to our document's extent.
            SetVerticalOffsetInternal(ExtentHeight);
        }
 
        /// <summary>
        /// Sets the scale factor applied to pages in the document, while
        /// keeping the "Active Focus" centered.
        /// </summary>
        /// <param name="scale"></param>
        public void SetScale(double scale)
        {
            if (!DoubleUtil.AreClose(scale, Scale))
            {
                ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(scale, 0.0);
 
                if (!Helper.IsDoubleValid(scale))
                {
                    throw new ArgumentOutOfRangeException("scale");
                }
 
                QueueSetScale(scale);
            }
        }
 
        /// <summary>
        /// Changes the view to the specified number of columns.
        /// </summary>
        /// <param name="columns"></param>
        public void SetColumns(int columns)
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(columns, 1);
 
            //Perf Tracing - Mark Layout Change Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLayoutBegin);
 
            QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.SetColumns));
        }
 
        /// <summary>
        /// Changes the view to the specified number of columns.
        /// </summary>
        /// <param name="columns"></param>
        public void FitColumns(int columns)
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(columns, 1);
 
            //Perf Tracing - Mark Layout Change Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLayoutBegin);
 
            QueueUpdateDocumentLayout(new DocumentLayout(columns, ViewMode.FitColumns));
        }
 
        /// <summary>
        /// Changes the view to a single page, scaled such that it is as wide as the Viewport.
        /// </summary>
        public void FitToPageWidth()
        {
            //Perf Tracing - Mark Layout Change Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLayoutBegin);
 
            QueueUpdateDocumentLayout(
                new DocumentLayout(1 /* one column */, ViewMode.PageWidth));
        }
 
        /// <summary>
        /// Changes the view to a single page, scaled such that it is as tall as the Viewport.
        /// </summary>
        public void FitToPageHeight()
        {
            //Perf Tracing - Mark Layout Change Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLayoutBegin);
 
            QueueUpdateDocumentLayout(
                new DocumentLayout(1 /* one column */, ViewMode.PageHeight));
        }
 
        /// <summary>
        /// Changes the view to ?thumbnail view? which will scale the document
        /// such that as many pages are visible at once as is possible.
        /// </summary>
        public void ViewThumbnails()
        {
            //Perf Tracing - Mark Layout Change Start
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXLayoutBegin);
 
            QueueUpdateDocumentLayout(
                new DocumentLayout(1 /* one column, arbitrary */, ViewMode.Thumbnails));
        }
 
        //------------------------------------------------------
        //
        //  IDocumentScrollInfo Properties
        //
        //------------------------------------------------------
 
        /// <summary>
        /// DocumentGrid always scrolls in both dimensions.
        /// </summary>
        public bool CanHorizontallyScroll
        {
            get { return _canHorizontallyScroll; }
            set { _canHorizontallyScroll = value; }
        }
 
        /// <summary>
        /// DocumentGrid always scrolls in both dimensions.
        /// </summary>
        public bool CanVerticallyScroll
        {
            get { return _canVerticallyScroll; }
            set { _canVerticallyScroll = value; }
        }
 
        /// <summary>
        /// ExtentWidth contains the full horizontal range of the scrolled content.
        /// </summary>
        public double ExtentWidth
        {
            get
            {
                return _rowCache.ExtentWidth;
            }
        }
 
        /// <summary>
        /// ExtentHeight contains the full vertical range of the scrolled content.
        /// </summary>
        public double ExtentHeight
        {
            get
            {
                return _rowCache.ExtentHeight;
            }
        }
 
        /// <summary>
        /// ViewportWidth contains the currently visible horizontal range of the scrolled content.
        /// </summary>
        public double ViewportWidth
        {
            get
            {
                return _viewportWidth;
            }
        }
 
        /// <summary>
        /// ViewportHeight contains the currently visible vertical range of the scrolled content.
        /// </summary>
        public double ViewportHeight
        {
            get
            {
                return _viewportHeight;
            }
        }
 
        /// <summary>
        /// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible.
        /// Valid values are inclusively between 0 and <see cref="ExtentWidth" /> less <see cref="ViewportWidth" />.
        /// </summary>
        public double HorizontalOffset
        {
            get
            {
                //Clip HorizontalOffset into range.
                double clippedHorizontalOffset = Math.Min(_horizontalOffset, ExtentWidth - ViewportWidth);
                clippedHorizontalOffset = Math.Max(clippedHorizontalOffset, 0.0);
 
                return clippedHorizontalOffset;
            }
        }
 
        /// <summary>
        /// Set the HorizontalOffset.  If there are pending layout delegates, then
        /// this will be processed by a delegate.
        /// </summary>
        /// <param name="offset"></param>
        public void SetHorizontalOffset(double offset)
        {
            if (!DoubleUtil.AreClose(_horizontalOffset, offset))
            {
                if (Double.IsNaN(offset))
                {
                    throw new ArgumentOutOfRangeException("offset");
                }
 
                // If there aren't any pending document layout delegates, then change
                // the HorizontalOffset immediately, otherwise schedule a delegate for it.
                if (_documentLayoutsPending == 0)
                {
                    SetHorizontalOffsetInternal(offset);
                }
                else
                {
                    QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetHorizontalOffset));
                }
            }
        }
 
        /// <summary>
        /// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible.
        /// Valid values are inclusively between 0 and <see cref="ExtentHeight" /> less <see cref="ViewportHeight" />.
        /// </summary>
        public double VerticalOffset
        {
            get
            {
                //Clip VerticalOffset into range.
                double clippedVerticalOffset = Math.Min(_verticalOffset, ExtentHeight - ViewportHeight);
                clippedVerticalOffset = Math.Max(clippedVerticalOffset, 0.0);
 
                return clippedVerticalOffset;
            }
        }
 
        /// <summary>
        /// Set the VerticalOffset.  If there are pending layout delegates, then
        /// this will be processed by a delegate.
        /// </summary>
        /// <param name="offset"></param>
        public void SetVerticalOffset(double offset)
        {
            if (!DoubleUtil.AreClose(_verticalOffset, offset))
            {
                if (Double.IsNaN(offset))
                {
                    throw new ArgumentOutOfRangeException("offset");
                }
 
                // If there aren't any pending document layout delegates, then change
                // the VerticalOffset immediately, otherwise schedule a delegate for it.
                if (_documentLayoutsPending == 0)
                {
                    SetVerticalOffsetInternal(offset);
                }
                else
                {
                    QueueUpdateDocumentLayout(new DocumentLayout(offset, ViewMode.SetVerticalOffset));
                }
            }
        }
 
        /// <summary>
        /// Provides the IDocumentScrollInfo implementer with a content
        /// tree to be paginated.  Developers are free to modify this
        /// Content at any time (remove, add, modify pages, etc?)
        /// and the IDocumentScrollInfo implementer is responsible for
        /// noting the changes and updating as necessary.
        /// </summary>
        /// <value>The DocumentPaginator to be assigned as the content</value>
        public DynamicDocumentPaginator Content
        {
            get
            {
                //_pageCache is guaranteed to be non-null as it's created in the
                //Constructor.
                return _pageCache.Content;
            }
            set
            {
                //_pageCache is guaranteed to be non-null as it's created in the
                //Constructor.
                if (value != _pageCache.Content)
                {
                    //Null out our TextContainer.  It will be created as needed.
                    _textContainer = null;
 
                    //Remove our old events from the content
                    if (_pageCache.Content != null)
                    {
                        _pageCache.Content.GetPageNumberCompleted -= new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted);
                    }
 
                    //Remove our ScrollChanged events from our ScrollViewer
                    if (ScrollOwner != null)
                    {
                        ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged);
                        _scrollChangedEventAttached = false;
                    }
 
                    //Assign the new content
                    _pageCache.Content = value;
 
                    if (_pageCache.Content != null)
                    {
                        //Add our new events to the content
                        _pageCache.Content.GetPageNumberCompleted += new GetPageNumberCompletedEventHandler(OnGetPageNumberCompleted);
                    }
 
                    //Clear out our visual collection so that the old pages (pointing to old content)
                    //will be replaced with new ones on the next Measure/Arrange pass.
                    ResetVisualTree(false /*pruneOnly*/);
                    ResetPageViewCollection();
 
                    //Reset our visible pages.
                    _firstVisiblePageNumber = 0;
                    _lastVisiblePageNumber = 0;
 
                    // Perf Tracing - PageVisible Changed
                    EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageVisible, _firstVisiblePageNumber, _lastVisiblePageNumber);
 
                    _lastRowChangeExtentWidth = 0.0;
                    _lastRowChangeVerticalOffset = 0.0;
 
                    //Cause the new content to be laid out in the same fashion as
                    //the previous content.
                    //If the view is Thumbnails we'll change it to SetColumns
                    //so that the Column count will be maintained.  This is done
                    //because we're getting new content which initially has 0
                    //pages and a Thumbnail view only gives decent results after
                    //the entire content has been loaded; since we don't want to provide a
                    //"jarring" situation (where layout suddenly changes after the content's loaded)
                    //we use SetColumns to maintain the same exact layout as the old content.
                    //(This is consistent with DocumentViewer's overall behavior -- any view setting is
                    //a "one time thing" and isn't recomputed if the content changes.)
                    if (_documentLayout.ViewMode == ViewMode.Thumbnails)
                    {
                        _documentLayout.ViewMode = ViewMode.SetColumns;
                    }
                    QueueUpdateDocumentLayout(_documentLayout);
 
                    //Invalidate Measure and our IDSI so that properties changed
                    //by the content assignment will be properly updated.
                    InvalidateMeasure();
                    InvalidateDocumentScrollInfo();
                }
            }
        }
 
        /// <summary>
        /// Indicates the number of pages currently in the document.
        /// </summary>
        /// <value></value>
        public int PageCount
        {
            get
            {
                return _pageCache.PageCount;
            }
        }
 
        /// <summary>
        /// When queried, FirstVisiblePageNumber returns the first page visible onscreen.
        /// </summary>
        /// <value></value>
        public int FirstVisiblePageNumber
        {
            get
            {
                return _firstVisiblePageNumber;
            }
        }
 
        /// <summary>
        /// Returns the current Scale factor applied to the pages given the current settings.
        /// </summary>
        /// <value></value>
        public double Scale
        {
            get
            {
                return _rowCache.Scale;
            }
        }
 
        /// <summary>
        /// Returns the current number of Columns of pages displayed given the current settings.
        /// </summary>
        /// <value></value>
        public int MaxPagesAcross
        {
            get
            {
                return _maxPagesAcross;
            }
        }
 
        /// <summary>
        /// Specifies the vertical gap between Pages when laid out, in pixel (1/96?) units.
        /// </summary>
        /// <value></value>
        public double VerticalPageSpacing
        {
            get
            {
                return _rowCache.VerticalPageSpacing;
            }
 
            set
            {
                if (!Helper.IsDoubleValid(value))
                {
                    throw new ArgumentOutOfRangeException("value");
                }
 
                _rowCache.VerticalPageSpacing = value;
            }
        }
 
        /// <summary>
        /// Specifies the horizontal gap between Pages when laid out, in pixel (1/96?) units.
        /// </summary>
        /// <value></value>
        public double HorizontalPageSpacing
        {
            get
            {
                return _rowCache.HorizontalPageSpacing;
            }
 
            set
            {
                if (!Helper.IsDoubleValid(value))
                {
                    throw new ArgumentOutOfRangeException("value");
                }
 
                _rowCache.HorizontalPageSpacing = value;
            }
        }
 
        /// <summary>
        /// Specifies whether each displayed page should be adorned with a ?Drop Shadow? border or not.
        /// </summary>
        /// <value></value>
        public bool ShowPageBorders
        {
            get
            {
                return _showPageBorders;
            }
 
            set
            {
                if (_showPageBorders != value)
                {
                    _showPageBorders = value;
 
                    //Update our pages' ShowPageBorder properties.
                    //Get the current Visual Collection, which contains our pages.
                    int count = _childrenCollection.Count;
                    for (int i = 0; i < count; i++)
                    {
                        DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage;
 
                        if (dp != null)
                        {
                            dp.ShowPageBorders = _showPageBorders;
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Specifies whether the last "view mode" related property change should be locked
        /// for resizing.
        /// </summary>
        /// <value></value>
        public bool LockViewModes
        {
            get
            {
                return _lockViewModes;
            }
 
            set
            {
                _lockViewModes = value;
            }
        }
 
        /// <summary>
        /// Returns a TextContainer for current content
        /// </summary>
        /// <returns>The content's TextContainer, or null if there is none.</returns>
        public ITextContainer TextContainer
        {
            get
            {
                if (_textContainer == null)
                {
                    if (Content != null)
                    {
                        IServiceProvider isp = Content as IServiceProvider;
                        if (isp != null)
                        {
                            _textContainer = (ITextContainer)isp.GetService(typeof(ITextContainer));
                        }
                    }
                }
 
                return _textContainer;
            }
        }
 
        /// <summary>
        /// Returns the MultiPageTextView for the current content.
        /// </summary>
        /// <value></value>
        public ITextView TextView
        {
            get
            {
                if (TextEditor != null)
                {
                    return TextEditor.TextView;
                }
                else
                {
                    return null;
                }
            }
        }
 
        /// <summary>
        /// The collection of currently-visible DocumentPageViews.
        /// </summary>
        public ReadOnlyCollection<DocumentPageView> PageViews
        {
            get
            {
                return _pageViews;
            }
        }
 
        /// <summary>
        /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependent
        /// on this IScrollInfo's properties.  Implementers of IScrollInfo should call InvalidateScrollInfo()
        /// on this object when related properties change.
        /// </summary>
        public ScrollViewer ScrollOwner
        {
            get
            {
                return _scrollOwner;
            }
 
            set
            {
                _scrollOwner = value;
                InvalidateDocumentScrollInfo();
            }
        }
 
        /// <summary>
        /// DocumentViewerOwner is the DocumentViewer Control and UI that hosts the IDocumentScrollInfo object.
        /// This control is dependent on this IDSI?s properties, so implementers of IDSI should call
        /// InvalidateDocumentScrollInfo() on this object when related properties change so that
        /// DocumentViewer?s UI will be kept in sync.  This property is analogous to IScrollInfo?s ScrollOwner
        /// property.
        /// </summary>
        /// <value></value>
        public DocumentViewer DocumentViewerOwner
        {
            get
            {
                return _documentViewerOwner;
            }
 
            set
            {
                _documentViewerOwner = value;
            }
        }
 
        #endregion IDocumentScrollInfo
 
        #endregion Interface Implementations
 
        //------------------------------------------------------
        //
        //  Protected Methods
        //
        //------------------------------------------------------
        #region Protected Methods
 
        /// <summary>
        ///   Derived class must implement to support Visual children. The method must return
        ///    the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1.
        ///
        ///    By default a Visual does not have any children.
        ///
        ///  Remark:
        ///       During this virtual call it is not valid to modify the Visual tree.
        /// </summary>
        protected override Visual GetVisualChild(int index)
        {
            if (_childrenCollection == null || index < 0 || index >= _childrenCollection.Count)
            {
                throw new ArgumentOutOfRangeException("index", index, SR.Visual_ArgumentOutOfRange);
            }
 
            return _childrenCollection[index];
        }
 
        /// <summary>
        ///  Derived classes override this property to enable the Visual code to enumerate
        ///  the Visual children. Derived classes need to return the number of children
        ///  from this method.
        ///
        ///    By default a Visual does not have any children.
        ///
        ///  Remark: During this virtual method the Visual tree must not be modified.
        /// </summary>
        protected override int VisualChildrenCount
        {
            get
            {
                // _childrenCollection cannot be null since its initialized in the constructor
                return _childrenCollection.Count;
            }
        }
 
        /// <summary>
        /// MeasureOverride is repsonsible for measuring any visible pages to their correct sizes.
        /// </summary>
        /// <param name="constraint">The upper bound for child sizes</param>
        /// <returns></returns>
        protected override Size MeasureOverride(Size constraint)
        {
            // If layoutSize is infinity, we need to return our absolute smallest size.
            // This might happen if we are inside an element which sizes-to-content.
            // For DocumentGrid, we use a hard coded constraint.
            if (double.IsInfinity(constraint.Width) || double.IsInfinity(constraint.Height))
            {
                constraint = _defaultConstraint;
            }
 
            //Determine which pages are visible at the current offset given the current constraint.
            RecalculateVisualPages(VerticalOffset, constraint);
 
            //Get our visual children count...
            int count = _childrenCollection.Count;
 
            //Now go through our child collection and measure all the pages to their sizes.
            for (int i = 0; i < count; i++)
            {
                //This should be our background.
                if (i == _backgroundVisualIndex)
                {
                    Border background = _childrenCollection[i] as Border;
 
                    if (background == null)
                    {
                        throw new InvalidOperationException(SR.DocumentGridVisualTreeContainsNonBorderAsFirstElement);
                    }
 
                    //We measure this to the size of our constraint.
                    background.Measure(constraint);
                }
                //Otherwise it's a page.
                else
                {
                    //Ensure that this is actually a DocumentGridPage.  If it is not,
                    //Then someone's been mucking with our VisualTree, so we'll throw.
                    DocumentGridPage page = _childrenCollection[i] as DocumentGridPage;
 
                    if (page == null)
                    {
                        throw new InvalidOperationException(SR.DocumentGridVisualTreeContainsNonDocumentGridPage);
                    }
 
                    //Get the cached size of this page and scale it to our current scale factor.
                    Size pageSize = _pageCache.GetPageSize(page.PageNumber);
                    pageSize.Width *= Scale;
                    pageSize.Height *= Scale;
 
                    //Measure the page if necessary.
                    if (!page.IsMeasureValid)
                    {
                        page.Measure(pageSize);
 
                        //See if the cached size has changed since we Measured.
                        //This can happen if in the course of Measuring the page
                        //a GetPageAsync() calls back immediately with the real page size.
                        //If this happens we need to re-measure the page before we finish here,
                        //otherwise we'll end up with a page that's Measured to one size
                        //and Arranged to another, which looks bad.
                        Size newPageSize = _pageCache.GetPageSize(page.PageNumber);
                        if (newPageSize != Size.Empty)
                        {
                            newPageSize.Width *= Scale;
                            newPageSize.Height *= Scale;
                            if (newPageSize.Width != pageSize.Width ||
                            newPageSize.Height != pageSize.Height)
                            {
                                //Measure again.
                                page.Measure(newPageSize);
                            }
                        }
                    }
                }
            }
 
            return constraint;
        }
 
        /// <summary>
        /// ArrangeOverride is responsible for arranging the previously measured pages in the right order.
        /// </summary>
        /// <param name="arrangeSize">The final constraint, inside of which everything must live.</param>
        /// <returns></returns>
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            if (_viewportHeight != arrangeSize.Height ||
                _viewportWidth != arrangeSize.Width)
            {
                //Update our Viewport sizes
                _viewportWidth = arrangeSize.Width;
                _viewportHeight = arrangeSize.Height;
 
                if (LockViewModes && IsViewLoaded())
                {
                    if (_firstVisiblePageNumber < _pageCache.PageCount && _rowCache.HasValidLayout)
                    {
                        //If we're locking the view modes and we have loaded content, then we need to re-apply
                        //the last mode setting since our constraint has changed
                        ApplyViewParameters(_rowCache.GetRowForPageNumber(_firstVisiblePageNumber));
                        MeasureOverride(arrangeSize);
                    }
                }
 
                UpdateTextView();
            }
 
            //If we have a non-zero viewport size, we should execute any
            //requests for layout that may have been made but were unable
            //to complete due to a zero viewport size.
            if (IsViewportNonzero)
            {
                if (ExecutePendingLayoutRequests())
                {
                    //We need to re-do layout (RowCache has changed), so call measure here
                    //to ensure everything's updated accordingly.
                    MeasureOverride(arrangeSize);
                }
            }
 
            //If our constraint size has changed then we need to
            //alert our parents so they can update their ViewportWidth/Height properties.
            if (_previousConstraint != arrangeSize)
            {
                _previousConstraint = arrangeSize;
                InvalidateDocumentScrollInfo();
            }
 
            //Now we go through the visible rows and arrange the pages within them.
            //Get our visual collection count
            int count = _childrenCollection.Count;
 
            //If we have no visual children, there's nothing to arrange
            //so quit now.
            if (count == 0)
            {
                return arrangeSize;
            }
 
            //Arrange the background first.  This is always child 0.
            //The background takes up the entire constraint.
            UIElement background = _childrenCollection[_backgroundVisualIndex] as UIElement;
            background.Arrange(new Rect(new Point(0, 0), arrangeSize));
 
            //The offsets for the current page being arranged.
            double xOffset;
            double yOffset;
 
            //The current visual child (aka DocumentGridPage) we're arranging.
            //The first child in our tree is always the background so we start at
            //1 which is our first page.
            int visualChild = _firstPageVisualIndex;
 
            //Now walk through the visible rows and arrange the pages therein.
            for (int row = _firstVisibleRow; row < _firstVisibleRow + _visibleRowCount; row++)
            {
                //Calculate the position for this row.
                CalculateRowOffsets(row, out xOffset, out yOffset);
 
                //Get the current row.
                RowInfo currentRow = _rowCache.GetRow(row);
 
                //Now we can lay out this row.
                for (int page = currentRow.FirstPage; page < currentRow.FirstPage + currentRow.PageCount; page++)
                {
                    //This should never, ever happen so we'll throw if it does.
                    if (visualChild > _childrenCollection.Count - 1)
                    {
                        throw new InvalidOperationException(SR.DocumentGridVisualTreeOutOfSync);
                    }
 
                    //Get the cached size of this page.
                    Size pageSize = _pageCache.GetPageSize(page);
 
                    //Scale it by our scale factor
                    pageSize.Width *= Scale;
                    pageSize.Height *= Scale;
 
                    //Arrange the page if necessary
                    UIElement uiPage = _childrenCollection[visualChild] as UIElement;
                    if (uiPage != null)
                    {
                        Point pageOffset;
                        //Move the page to the right place based on the FlowDirection of the content.
                        if (_pageCache.IsContentRightToLeft)
                        {
                            pageOffset = new Point(Math.Max(ViewportWidth, ExtentWidth) - (xOffset + pageSize.Width), yOffset);
                        }
                        else
                        {
                            pageOffset = new Point(xOffset, yOffset);
                        }
                        uiPage.Arrange(new Rect(pageOffset, pageSize));
                    }
                    else
                    {
                        throw new InvalidOperationException(SR.DocumentGridVisualTreeContainsNonUIElement);
                    }
 
                    //Increment our horizontal offset to point to where the next page should go.
                    xOffset += (pageSize.Width + HorizontalPageSpacing);
 
                    //Move to the next page.
                    visualChild++;
                }
            }
 
            // As we scroll we need to keep the AdornerLayer up-to-date.
            // This ensures that annotation components scroll with the content.
            AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
            if (layer != null && layer.GetAdorners(this) != null)
                layer.Update(this);
 
            return arrangeSize;
        }
 
        /// <summary>
        /// Override the OnPreviewMouseLeftButtonDown method so that we can trap the
        /// keyboard+mouse events needed for Rubberband selection.
        /// Clicking the Left mouse button while holding Alt will enable the Rubberband
        /// selection "mode" until the Left mouse button is again pressed without the
        /// Alt key held.
        /// </summary>
        /// <param name="e">The MouseButtonEventArgs associated with this mouse event.</param>
        protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            //Determine whether either Alt key is being held at this moment.
            bool altKeyDown = Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt);
 
            //If the Alt key is held and we aren't currently in RubberBandSelection mode,
            //we can create and attach our RubberBandSelector now.
            //We'll stay in this mode until the mouse is clicked without the Alt key held.
            if (altKeyDown && _rubberBandSelector == null)
            {
                //See if our content implements IServiceProvider.
                IServiceProvider serviceProvider = Content as IServiceProvider;
                if (serviceProvider != null)
                {
                    //See if our content supports rubber band selection.
                    _rubberBandSelector = serviceProvider.GetService(typeof(RubberbandSelector)) as RubberbandSelector;
 
                    if (_rubberBandSelector != null)
                    {
                        DocumentViewerOwner.Focus(); // text editor needs to be focused when cleared
                        ITextRange textRange = TextEditor.Selection;
                        textRange.Select(textRange.Start, textRange.Start); //clear selection
                        DocumentViewerOwner.IsSelectionEnabled = false;
 
                        _rubberBandSelector.AttachRubberbandSelector((FrameworkElement)this); //attach the Rubber band selector.
                    }
                }
            }
            //We got a mouse-down event and the Alt key is not being held, so we revert back
            //to normal selection mode now.
            else if (!altKeyDown && _rubberBandSelector != null)
            {
                //Detach the Rubberband Selector
                if (_rubberBandSelector != null)
                {
                    _rubberBandSelector.DetachRubberbandSelector();
                    _rubberBandSelector = null;
                }
 
                DocumentViewerOwner.IsSelectionEnabled = true;
            }
        }
 
        /// <summary>
        ///
        /// Reset the entire visual tree when the visual parent changes in order to ensure that
        /// the HighlightVisuals associated with the FixedPages in the document are re-created
        /// and added to the new AdornerLayer.
        /// </summary>
        /// <param name="oldParent">The old visual parent (not used)</param>
        protected internal override void OnVisualParentChanged(DependencyObject oldParent)
        {
            base.OnVisualParentChanged(oldParent);
 
            // No need for a reset if we don't have a parent since there is no AdornerLayer
            // to add HighlightVisuals back to.
            if (VisualTreeHelper.GetParent(this) != null)
            {
                // Do a full reset, we want to ensure even visible pages are reset.
                ResetVisualTree(pruneOnly: false);
            }
        }
 
        #endregion Protected Methods
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
        #region Private Methods
 
        /// <summary>
        /// Recalculates the set of pages that are currently visible and updates
        /// DocumentGrid's VisualCollection so it contains them.
        /// </summary>
        /// <param name="constraint">The viewport to search for visible pages in.</param>
        /// <param name="offset">The offset in the document to start the search.</param>
        private void RecalculateVisualPages(double offset, Size constraint)
        {
            //Do we actually have any rows in the cache?
            //If not, we can just clear our visual collection and return.
            if (_rowCache.RowCount == 0)
            {
                ResetVisualTree(false /*pruneOnly*/);
                ResetPageViewCollection();
                _firstVisibleRow = 0;
                _visibleRowCount = 0;
                _firstVisiblePageNumber = 0;
                _lastVisiblePageNumber = 0;
 
                // Perf Tracing - PageVisible Changed
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageVisible, _firstVisiblePageNumber, _lastVisiblePageNumber);
 
                return;
            }
 
            int newFirstVisibleRow = 0;
            int newVisibleRowCount = 0;
 
            //Ask the RowCache for the currently visible rows.
            _rowCache.GetVisibleRowIndices(offset,
                                            offset + constraint.Height,
                                            out newFirstVisibleRow,
                                            out newVisibleRowCount);
 
            //Do we have no visible rows at all?  Then clear the Visual collection and return.
            if (newVisibleRowCount == 0)
            {
                ResetVisualTree(false /*pruneOnly*/);
                ResetPageViewCollection();
                _firstVisibleRow = 0;
                _visibleRowCount = 0;
                _firstVisiblePageNumber = 0;
                _lastVisiblePageNumber = 0;
 
                // Perf Tracing - PageVisible Changed
                EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageVisible, _firstVisiblePageNumber, _lastVisiblePageNumber);
 
                return;
            }
 
            //Now walk through each visible row and compare the pages therein
            //with the current set of pages in our Visual Collection.
            //New pages are inserted into the collection, unused pages are removed.
 
            //Get the current first and last pages visible.
            int firstPage = -1;
            int lastPage = -1;
 
            //If we have more visuals than just the background (element 0)
            //then we have pages, so get the page numbers from them.
            if (_childrenCollection.Count > _firstPageVisualIndex)
            {
                DocumentGridPage firstDp = _childrenCollection[1] as DocumentGridPage;
                firstPage = firstDp != null ? firstDp.PageNumber : -1;
 
                DocumentGridPage lastDp = _childrenCollection[_childrenCollection.Count - 1] as DocumentGridPage;
                lastPage = lastDp != null ? lastDp.PageNumber : -1;
            }
 
 
            //Update our First & LastVisiblePage properties
            RowInfo firstRow = _rowCache.GetRow(newFirstVisibleRow);
            _firstVisiblePageNumber = firstRow.FirstPage;
            RowInfo lastRow = _rowCache.GetRow(newFirstVisibleRow + newVisibleRowCount - 1);
            _lastVisiblePageNumber = lastRow.FirstPage + lastRow.PageCount - 1;
 
            // Perf Tracing - PageVisible Changed
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXPageVisible, _firstVisiblePageNumber, _lastVisiblePageNumber);
 
            //Update our cached visible row info (used by Measure/Arrange)
            _firstVisibleRow = newFirstVisibleRow;
            _visibleRowCount = newVisibleRowCount;
 
 
            //If IDSI properties have changed (namely the First/LastVisiblePage properties) we invalidate them now.
            if (_firstVisiblePageNumber != firstPage ||
                _lastVisiblePageNumber != lastPage)
            {
                //Create our temporary VisualCollection, which will hold the new list of
                //visible pages.
                ArrayList visiblePages = new ArrayList();
 
                //Now walk through the visible rows and add the pages to our temporary list.
                for (int i = _firstVisibleRow; i < _firstVisibleRow + _visibleRowCount; i++)
                {
                    //Get the row
                    RowInfo currentRow = _rowCache.GetRow(i);
 
                    //Walk through the row
                    for (int j = currentRow.FirstPage; j < currentRow.FirstPage + currentRow.PageCount; j++)
                    {
                        //Is this page new?
                        if (j < firstPage || j > lastPage || _childrenCollection.Count <= _firstPageVisualIndex)
                        {
                            //Create a new page and add it to our temporary visual collection.
                            DocumentGridPage dp = new DocumentGridPage(Content);
                            dp.ShowPageBorders = ShowPageBorders;
                            dp.PageNumber = j;
 
                            //Attach the Loaded event handler
                            dp.PageLoaded += new EventHandler(OnPageLoaded);
                            visiblePages.Add(dp);
                        }
                        else
                        {
                            //This page already exists in our visual collection, so we copy that entry over
                            //from the visual collection instead of creating a new page.
                            //(We start at 1 to skip over the background visual.)
                            visiblePages.Add(_childrenCollection[_firstPageVisualIndex + j - Math.Max(0, firstPage)]);
                        }
                    }
                }
 
                //Copy our new visible page collection over to the VisualCollection and update
                //the MultiPageTextView's list of visible DocumentPageViews.
                //First, prune our visual tree so it only contains the set of pages that are visible
                //before and after the layout change.
                ResetVisualTree(true /*pruneOnly*/);
 
                Collection<DocumentPageView> documentPageViews =
                    new Collection<DocumentPageView>();
 
                //State machine for updating visual collection without removing still-visible pages
                //from the collection:
                //We walk through the set of visible pages that we computed above.
                //We insert new pages before existing pages, and add pages after existing pages.
                //
                //We take advantage of the fact that both the current set of pages in the Visual Collection
                //and the set of to-be-made visible pages in visiblePages are in strictly increasing order
                //with no gaps between pages.
                //To better understand how the below works, refer to Fig. A below:
                //
                //         +-----------------------------+
                //  +------+                             +------+
                //  | new  |  Pruned Visual Collection   |  new |
                //  +------+   (Existing Page Visuals)   +------+
                //         +-----------------------------+
                //  ^      ^                             ^
                //  |      |                             |
                //  A      B                             C
                //
                //                                 [Fig. A: Diagram of states]
                //
                //- The routine starts off in state A (BeforeExisting).
                //  At this point we insert any new pages in the visiblePages collection until we
                //  find a page in the visiblePages collection that is also in the Pruned Visual Collection.
                //  This indicates that the set of common unchanged pages has been reached.
                //  The state machine then transitions to state B (DuringExisting).
                //- The routine stays in B merely iterating through visiblePages until it finds a page
                //  in visiblePages that does not correspond to a page in the Visual Tree.  This indicates
                //  that the end of the set of common unchanged pages has been reached; at this point we
                //  add the new page and transition to state C (AfterExisting).
                //- State C ends when no more pages are left in visiblePages.
                VisualTreeModificationState state = VisualTreeModificationState.BeforeExisting;
 
                //The index pointing to the first common page still in the visual tree after the above pruning.
                int vcIndex = _firstPageVisualIndex;
 
                for (int i = 0; i < visiblePages.Count; i++)
                {
                    Visual current = (Visual)visiblePages[i];
 
                    switch (state)
                    {
                        case VisualTreeModificationState.BeforeExisting:
                            //Keep inserting until we find a page that already exists
                            if (vcIndex < _childrenCollection.Count && _childrenCollection[vcIndex] == current)
                            {
                                //Move to "During" state
                                state = VisualTreeModificationState.DuringExisting;
                            }
                            else
                            {
                                //Insert this page at the current index.
                                _childrenCollection.Insert(vcIndex, current);
                            }
                            //Increment the index into the Visual collection to ensure that it continues
                            //to point to the first common page.
                            vcIndex++;
                            break;
 
                        case VisualTreeModificationState.DuringExisting:
                            //Leave the visual collection alone until we find a page that isn't in the collection
                            //or run out of pages in the collection.
                            if (vcIndex >= _childrenCollection.Count || _childrenCollection[vcIndex] != current)
                            {
                                //Move to "After" state
                                state = VisualTreeModificationState.AfterExisting;
                                //Append this page to the end.
                                _childrenCollection.Add(current);
                            }
                            //Keep moving through the Visual collection...
                            vcIndex++;
                            break;
 
                        case VisualTreeModificationState.AfterExisting:
                            //Keep going until the end.
                            _childrenCollection.Add(current);
                            break;
                    }
 
                    //Add this to the collection of PageViews.
                    documentPageViews.Add(((DocumentGridPage)visiblePages[i]).DocumentPageView);
                }
 
                //Update our collection of PageViews with the current set.
                _pageViews = new ReadOnlyCollection<DocumentPageView>(documentPageViews);
 
                //Tell our parent DocumentViewer that we've updated our PageView collection.
                InvalidatePageViews();
                InvalidateDocumentScrollInfo();
            }
        }
 
        /// <summary>
        /// Handles the PageLoaded event for a given page, and kicks off
        /// BringIntoView actions where necessary.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnPageLoaded(object sender, EventArgs args)
        {
            DocumentGridPage page = sender as DocumentGridPage;
            Invariant.Assert(page != null, "Invalid sender for OnPageLoaded event.");
 
            //Detach the event handler, we don't need this event any longer.
            page.PageLoaded -= new EventHandler(OnPageLoaded);
 
            //Is there a MakeVisible operation waiting for this page to be loaded?
            //If so, invoke its dispatcher in the background.
            if (_makeVisiblePageNeeded == page.PageNumber)
            {
                _makeVisiblePageNeeded = -1;
                _makeVisibleDispatcher.Priority = DispatcherPriority.Background;
            }
 
            // Perf Tracing - PageLoaded
            if (EventTrace.IsEnabled(EventTrace.Keyword.KeywordXPS, EventTrace.Level.Info))
            {
                EventTrace.EventProvider.TraceEvent(EventTrace.Event.WClientDRXPageLoaded, EventTrace.Keyword.KeywordXPS, EventTrace.Level.Info, page.PageNumber);
            }
        }
 
 
        /// <summary>
        /// Calculates the X and Y offsets of the given row based on the current
        /// Viewport dimensions.
        /// </summary>
        /// <param name="row">The row to calculate the offsets of</param>
        /// <param name="xOffset">The X offset of the row</param>
        /// <param name="yOffset">The Y offset of the row</param>
        private void CalculateRowOffsets(int row, out double xOffset, out double yOffset)
        {
            xOffset = 0.0;
            yOffset = 0.0;
 
            //Get the current row.
            RowInfo currentRow = _rowCache.GetRow(row);
 
            //Figure out the width we'll use to center the pages.
            //If the ViewportWidth is wider than the document, we use that.  Otherwise we center
            //the content based on the width of the document.
            double centerWidth = Math.Max(ViewportWidth, ExtentWidth);
 
            //Figure out the offset of the upper left corner of this row.
 
            //X Coordinate:
            //If this is the last row in the document and we're viewing
            //uniformly-sized pages then this row is
            //always left-aligned (not centered).
            if (row == _rowCache.RowCount - 1 && !_pageCache.DynamicPageSizes)
            {
                //This is the last row, so we arrange it such that the left edge of the
                //page is flush with the left edge of the document.
                xOffset = (centerWidth - ExtentWidth) / 2.0 +
                    (HorizontalPageSpacing / 2.0) - HorizontalOffset;
            }
            else
            {
                //Otherwise we center this page inside the document.
                xOffset = (centerWidth - currentRow.RowSize.Width) / 2.0 +
                    (HorizontalPageSpacing / 2.0) - HorizontalOffset;
            }
 
            //Y Coordinate:
            if (ExtentHeight > ViewportHeight)
            {
                //The document is taller than the viewport, so we just display
                //the content at the current offset.
                yOffset = currentRow.VerticalOffset +
                    (VerticalPageSpacing / 2.0) - VerticalOffset;
            }
            else
            {
                //If the document is shorter than the Viewport we're showing it in,
                //we center it vertically within the viewport.  We do not need to factor in
                //VerticalOffset as it is always 0.0 in this scenario.
                yOffset = currentRow.VerticalOffset +
                    (ViewportHeight - ExtentHeight) / 2.0 + (VerticalPageSpacing / 2.0);
            }
        }
 
        /// <summary>
        /// Resets DocumentGrid's visual tree to its initial state or prunes non-visible pages.
        /// This is empty except for a border which acts as a background.
        /// </summary>
        /// <param name="pruneOnly">Whether to clear all pages, or only those that are not visible.</param>
        private void ResetVisualTree(bool pruneOnly)
        {
            //We need to dispose and remove any pages that will no longer be in the visual tree.
            for (int i = _childrenCollection.Count - 1; i >= _firstPageVisualIndex; i--)
            {
                DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage;
                if (dp != null &&
                        (!pruneOnly ||
                        _rowCache.RowCount == 0 ||
                        dp.PageNumber < _firstVisiblePageNumber ||
                        dp.PageNumber > _lastVisiblePageNumber))
                {
                    //This page will not be visible any longer, so get rid of it.
                    //Remove this page from the Visual tree.
                    _childrenCollection.Remove(dp);
 
                    //Remove any PageLoaded event handlers
                    dp.PageLoaded -= new EventHandler(OnPageLoaded);
 
                    //Dispose of the page.
                    ((IDisposable)dp).Dispose();
                }
            }
 
            //Create the background if it does not exist.
            if (_documentGridBackground == null)
            {
                //We create a Border with a transparent background so that it can
                //participate in Hit-Testing (which allows click events like those
                //for our Context Menu to work).
                _documentGridBackground = new Border();
                _documentGridBackground.Background = Brushes.Transparent;
 
                //Add the background in.
                _childrenCollection.Add(_documentGridBackground);
            }
        }
 
        /// <summary>
        /// Nulls out the PageViews collection and notifies DocumentViewer of the change.
        /// </summary>
        private void ResetPageViewCollection()
        {
            //Null out our collection of PageViews.
            _pageViews = null;
 
            //Tell our parent DocumentViewer that we've updated our PageView collection.
            InvalidatePageViews();
        }
 
        #region MakeVisible Helpers
 
        /// <summary>
        /// Handles the GetPageNumberCompleted event fired as a result of a MakeContentVisibleAsync
        /// call.  At this point we know the page number corresponding to the ContentPosition we need
        /// to make visible, so we invoke MakeVisibleAsync() to bring it into view.
        /// </summary>
        /// <param name="sender">The sender of this event</param>
        /// <param name="e">The args associated with this event.
        /// We expect e.UserState to be a MakeVisibleData.</param>
        private void OnGetPageNumberCompleted(object sender, GetPageNumberCompletedEventArgs e)
        {
            ArgumentNullException.ThrowIfNull(e);
            //Ensure that the UserState passed with this event contains an
            //MakeVisibleData object. If not, we ignore it as this event
            //could have originated from someone else calling GetPageNumberAsync.
            if (e.UserState is MakeVisibleData)
            {
                MakeVisibleData data = (MakeVisibleData)e.UserState;
                MakeVisibleAsync(data, e.PageNumber);
            }
        }
 
 
        /// <summary>
        /// Makes the specified object on the specified page visible, which may be an
        /// asynchronous operation if the page is not already in view.
        /// </summary>
        /// <param name="data">Data corresponding to the object to be made visible.</param>
        /// <param name="pageNumber">The page number the object is on.</param>
        private void MakeVisibleAsync(MakeVisibleData data, int pageNumber)
        {
            //This page may not be currently visible.
            //First we need to make the page visible, if necessary.
            //This will be done at background priority to allow currently-loading pages time to
            //finish.  If we do not do this, then in the corner case of:
            // 1) Document has just been loaded (for example, just after a hyperlink navigation to the doc)
            // 2) Document has non 8.5x11-sized pages at the beginning of the document
            // 3) Navigation is to a page past the initially visible pages.
            //In this case, the order of operations is something like this:
            // 1) Initially visible pages start loading
            // 2) MakeVisible is invoked, and MakePageVisible is called, which uses cached
            //    page info to decide where to scroll to.  (8.5x11 is assumed until a page is loaded)
            // 3) Document is scrolled to position computed in #2
            // 4) Pages loading in #1 finish loading, GetPageCompleted is called, PageCache is updated,
            //    and the document layout changes.  This will shift the page we actually want to see up or down,
            //    potentially by a substantial amount.
            // 5) User is now looking at the wrong page, and if the page that the hyperlink target is on isn't
            //    visible at this point then the MakeVisible operation fails in MakeVisibleImpl since the target
            //    isn't in the visual tree.
            // Everyone got that?  So, doing the initial "MakePageVisible" operation at Background priority
            // allows step #4 above to finish _before_ we do steps 2 and 3, so the right page will be visible
            // in 5 and the MakeVisible operation will succeed.
            Dispatcher.BeginInvoke(DispatcherPriority.Background,
                   new BringPageIntoViewCallback(BringPageIntoViewDelegate), data, pageNumber);
        }
 
        /// <summary>
        /// Delegate method used to bring a specified page into view.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="pageNumber"></param>
        private void BringPageIntoViewDelegate(MakeVisibleData data, int pageNumber)
        {
            //Make the page visible if necessary:
            // - If our layout is not yet valid
            // - If the visual being made visible is a FixedPage and
            //   the bring-into-view rect is the entire page
            //   (in which case we always want to move it as close to the top of the
            //   viewport as possible even if it is already partially visible)
            // - If the page isn't currently visible.
            if (!_rowCache.HasValidLayout ||
                (data.Visual is FixedPage &&
                 data.Visual.VisualContentBounds == data.Rect) ||
                pageNumber < _firstVisiblePageNumber ||
                pageNumber > _lastVisiblePageNumber)
            {
                MakePageVisible(pageNumber);
            }
 
            //The page's contents have already been loaded, we can bring the object into view immediately.
            if (IsPageLoaded(pageNumber))
            {
                MakeVisibleImpl(data);
            }
            else
            {
                //Now we have to wait for the page to be loaded so that we can
                //ensure that the object itself is visible.
                //As pages are loaded, this page will be checked for, and the dispatcher below
                //executed as appropriate.
                _makeVisiblePageNeeded = pageNumber;
                _makeVisibleDispatcher = Dispatcher.BeginInvoke(DispatcherPriority.Inactive,
                    (DispatcherOperationCallback)delegate (object arg)
                    {
                        MakeVisibleImpl((MakeVisibleData)arg);
                        return null;
                    }, data);
            }
        }
 
        /// <summary>
        /// Implementation of MakeVisible logic, the final step in a MakeVisible operation.
        /// </summary>
        /// <param name="data"></param>
        private void MakeVisibleImpl(MakeVisibleData data)
        {
            if (data.Visual != null)
            {
                //Ensure that the passed-in visual is a descendant of DocumentGrid.
                if (((Visual)this).IsAncestorOf(data.Visual))
                {
                    //Now we can determine where this visual is relative to the upper left
                    //corner of the DocumentGrid and thus make it visible.
                    GeneralTransform transform = data.Visual.TransformToAncestor(this);
                    Rect boundingRect = (data.Rect != Rect.Empty) ? data.Rect : data.Visual.VisualContentBounds;
 
                    Rect offsetRect = transform.TransformBounds(boundingRect);
                    MakeRectVisible(offsetRect, false /* alwaysCenter */);
                }
            }
            else if (data.ContentPosition != null)
            {
                ITextPointer tp = data.ContentPosition as ITextPointer;
 
                //If we have a valid TextView and the TextPointer is in that TextView
                //we can make the TextPointer's Rect visible...
                if (TextViewContains(tp))
                {
                    MakeRectVisible(TextView.GetRectangleFromTextPosition(tp), false /* alwaysCenter */);
                }
            }
            else
            {
                Invariant.Assert(false, "Invalid object brought into view.");
            }
        }
 
 
        /// <summary>
        /// Moves the specified rectangle into view, if it isn't already visible.
        /// </summary>
        /// <param name="r">A rectangle relative to the upper-left corner of the Viewport</param>
        /// <param name="alwaysCenter">Whether to center the rect at all times or only when necessary.</param>
        private void MakeRectVisible(Rect r, bool alwaysCenter)
        {
            if (r != Rect.Empty)
            {
                //Calculate the real position of the rectangle in the document.
                Rect translatedRect = new Rect(HorizontalOffset + r.X, VerticalOffset + r.Y,
                                               r.Width, r.Height);
 
                Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset,
                                             ViewportWidth,
                                             ViewportHeight);
 
                //Unless the alwaysCenter flag is set, if the new position is already
                //visible we don't need to shift the viewport. Otherwise we shift
                //the offsets so the rect is visible, centering if possible.
                if (alwaysCenter || !translatedRect.IntersectsWith(viewportRect))
                {
                    SetHorizontalOffsetInternal(translatedRect.X - (ViewportWidth / 2.0));
                    SetVerticalOffsetInternal(translatedRect.Y - (ViewportHeight / 2.0));
                }
            }
        }
 
        /// <summary>
        /// Moves the specified IP into view, if it isn't already visible.
        /// </summary>
        /// <param name="r">A rectangle relative to the upper-left corner of the Viewport which represents
        /// an IP (Insertion Point)</param>
        private void MakeIPVisible(Rect r)
        {
            if (r != Rect.Empty && TextEditor != null)
            {
                Rect viewportRect = new Rect(HorizontalOffset, VerticalOffset,
                                             ViewportWidth,
                                             ViewportHeight);
 
                //If the new position is already fully visible, we don't need to shift the viewport,
                //otherwise we shift the offsets so the rect is visible, moving as minimally as possible.
                if (!viewportRect.Contains(r))
                {
                    //Scroll left/right if the IP is off the screen Horizontally.
                    if (r.X < HorizontalOffset)
                    {
                        SetHorizontalOffsetInternal(HorizontalOffset - (HorizontalOffset - r.X));
                    }
                    else if (r.X > HorizontalOffset + ViewportWidth)
                    {
                        SetHorizontalOffsetInternal(HorizontalOffset + (r.X - (HorizontalOffset + ViewportWidth)));
                    }
 
                    //Scroll up/down if part of the IP is off the screen Vertically.
                    if (r.Y < VerticalOffset)
                    {
                        SetVerticalOffsetInternal(VerticalOffset - (VerticalOffset - r.Y));
                    }
                    else if (r.Y + r.Height > VerticalOffset + ViewportHeight)
                    {
                        SetVerticalOffsetInternal(VerticalOffset + ((r.Y + r.Height) - (VerticalOffset + ViewportHeight)));
                    }
                }
            }
        }
 
        /// <summary>
        /// Invokes GetPageNumberAsync on the passed in ContentPosition.
        /// The handler for GetPageNumberAsync will bring that ContentPosition into view.
        /// </summary>
        /// <param name="data">The MakeVisibleData to be made visible</param>
        private void MakeContentPositionVisibleAsync(MakeVisibleData data)
        {
            //If the ContentPosition is valid, we can make it visible now.
            if (data.ContentPosition != null && data.ContentPosition != ContentPosition.Missing)
            {
                Content.GetPageNumberAsync(data.ContentPosition, data);
            }
        }
 
        #endregion MakeVisible Helpers
 
        /// <summary>
        /// Places a delegate for an SetScale call on the queue.  We do this for
        /// performance reasons, as changing the document scale takes significant time.
        /// </summary>
        /// <param name="scale"></param>
        private void QueueSetScale(double scale)
        {
            //If there's a SetScale operation in the Pending state, then we'll
            //abort it (we only care that the last operation invoked completes.)
            if (_setScaleOperation != null &&
                _setScaleOperation.Status == DispatcherOperationStatus.Pending)
            {
                _setScaleOperation.Abort();
            }
            _setScaleOperation = Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input,
                   new DispatcherOperationCallback(SetScaleDelegate),
                   scale);
        }
 
        private object SetScaleDelegate(object scale)
        {
            if (!(scale is double))
            {
                return null;
            }
 
            double newScale = (double)scale;
            _documentLayout.ViewMode = ViewMode.Zoom;
 
            //Get the current visible selection, if any.
            //The results of this will determine how we handle the
            //zoom operation.
            ITextPointer selection = GetVisibleSelection();
 
            if (selection != null)
            {
                //The visible-IP case:
                //First, we find out what page the IP is on:
                int selectionPage = GetPageNumberForVisibleSelection(selection);
 
                //Then we scale the document:
                UpdateLayoutScale(newScale);
 
                //Now we ensure that the selection page is still
                //visible:
                MakePageVisible(selectionPage);
 
                //The rest of this process is done asynchronously --
                //We wait for LayoutUpdated (which happens after layout
                //but before rendering) and then make the IP visible.
                //This will cause the IP to be centered without any
                //visible flicker.
 
                //Attach a LayoutUpdated handler.
                LayoutUpdated += new EventHandler(OnZoomLayoutUpdated);
            }
            else
            {
                //The non-visible-IP case:
                //This is considerably easier.  The expected behavior is that
                //we zoom in on the upper-left corner of the currently-visible
                //content.  This is accomplished by scaling the Vertical and
                //Horizontal offsets in tandem with the document scale which will
                //put us approximately where we were before.
 
                //Scale the document:
                UpdateLayoutScale(newScale);
            }
 
            return null;
        }
 
        /// <summary>
        /// Updates the Scale applied to our RowCache.
        /// </summary>
        /// <param name="scale"></param>
        private void UpdateLayoutScale(double scale)
        {
            if (!DoubleUtil.AreClose(scale, Scale))
            {
                double oldExtentHeight = ExtentHeight;
                double oldExtentWidth = ExtentWidth;
 
                //Tell our RowCache to rescale the layout.
                _rowCache.Scale = scale;
 
                //Rescale our offsets
                //Divide the old extents by the new to determine the amount to
                //scale the offsets
                double verticalScale = oldExtentHeight == 0.0 ? 1.0 : ExtentHeight / oldExtentHeight;
                double horizontalScale = oldExtentWidth == 0.0 ? 1.0 : ExtentWidth / oldExtentWidth;
 
                //Now we scale the offsets.
                SetVerticalOffsetInternal(_verticalOffset * verticalScale);
                SetHorizontalOffsetInternal(_horizontalOffset * horizontalScale);
 
                InvalidateMeasure();
 
                //Invalidate the measure of our visual children so that they can
                //be resized.
                InvalidateChildMeasure();
 
                //Invalidate our parents' properties
                InvalidateDocumentScrollInfo();
            }
        }
 
        /// <summary>
        /// Places a delegate for an UpdateDocumentLayout call on the queue.  We do this for
        /// performance reasons, as changing the document layout takes significant time.
        /// </summary>
        /// <param name="layout"></param>
        private void QueueUpdateDocumentLayout(DocumentLayout layout)
        {
            // Increase the count of pending DocumentLayout delegates
            _documentLayoutsPending++;
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input,
                   new DispatcherOperationCallback(UpdateDocumentLayoutDelegate),
                   layout);
        }
 
        /// <summary>
        /// Asynchronously invokes UpdateDocumentLayout.
        /// </summary>
        /// <param name="layout"></param>
        /// <returns></returns>
        private object UpdateDocumentLayoutDelegate(object layout)
        {
            if (layout is DocumentLayout)
            {
                UpdateDocumentLayout((DocumentLayout)layout);
            }
            // Decrease the count of pending DocumentLayout delegates
            _documentLayoutsPending--;
 
            return null;
        }
 
        /// <summary>
        /// Updates the current layout of our RowCache to the specified number of
        /// columns.
        /// </summary>
        /// <param name="layout"></param>
        private void UpdateDocumentLayout(DocumentLayout layout)
        {
            // Check if the layout is for a Vertical or Horizontal offset update,
            // in which case the value can be changed immediately.
            if (layout.ViewMode == ViewMode.SetHorizontalOffset)
            {
                SetHorizontalOffsetInternal(layout.Offset);
                return;
            }
            else if (layout.ViewMode == ViewMode.SetVerticalOffset)
            {
                SetVerticalOffsetInternal(layout.Offset);
                return;
            }
 
            //Store off the layout in case we need it later.
            //(For example, if our viewport is (0,0).
            _documentLayout = layout;
 
            //Update MaxPagesAcross
            _maxPagesAcross = _documentLayout.Columns;
 
            //If we have a non (0,0) Viewport then we can calculate a new layout.
            //Otherwise we set our "Layout Requested" flag so that when we get
            //a non-zero Viewport size we'll compute the requested layout.
            if (IsViewportNonzero)
            {
                //If this is a Thumbnails layout, we need to calculate how many
                //columns we should fit on the pivotRow.
                if (_documentLayout.ViewMode == ViewMode.Thumbnails)
                {
                    _maxPagesAcross = _documentLayout.Columns = CalculateThumbnailColumns();
                }
 
                //We need to determine the page that has the active focus so we know what page
                //to keep visible in the new layout.
                int pivotPage = GetActiveFocusPage();
 
                //Ask the RowCache to recalculate the layout based
                //on the specified pivot page and the number of columns
                //requested.
                //The RowCache will call us back with a
                //RowLayoutCompleted event when the layout is complete
                //and we'll update the scale and invalidate our layout there.
                _rowCache.RecalcRows(pivotPage, _documentLayout.Columns);
 
                _isLayoutRequested = false;
            }
            else
            {
                _isLayoutRequested = true;
            }
        }
 
        /// <summary>
        /// Calls UpdateLayout with the saved column and view mode parameters,
        /// if there's a previously requested layout to perform.
        /// Used when we get a non-zero Viewport size and we've previously requested
        /// a new Row layout.
        /// </summary>
        /// <returns>A bool indicating whether a new layout was calculated.</returns>
        private bool ExecutePendingLayoutRequests()
        {
            if (_isLayoutRequested)
            {
                UpdateDocumentLayout(_documentLayout);
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Set the HorizontalOffset to the value provided, the value will be set immediately.
        /// </summary>
        /// <param name="offset"></param>
        private void SetHorizontalOffsetInternal(double offset)
        {
            if (!DoubleUtil.AreClose(_horizontalOffset, offset))
            {
                if (Double.IsNaN(offset))
                {
                    throw new ArgumentOutOfRangeException("offset");
                }
 
                _horizontalOffset = offset;
                InvalidateMeasure();
                InvalidateDocumentScrollInfo();
                UpdateTextView();
            }
        }
 
        /// <summary>
        /// Set the VerticalOffset to the value provided, the value will be set immediately.
        /// </summary>
        /// <param name="offset"></param>
        private void SetVerticalOffsetInternal(double offset)
        {
            if (!DoubleUtil.AreClose(_verticalOffset, offset))
            {
                if (Double.IsNaN(offset))
                {
                    throw new ArgumentOutOfRangeException("offset");
                }
 
                _verticalOffset = offset;
                InvalidateMeasure();
                InvalidateDocumentScrollInfo();
                UpdateTextView();
            }
        }
 
        /// <summary>
        /// Updates the TextView so that it knows about size and position changes.
        /// </summary>
        private void UpdateTextView()
        {
            MultiPageTextView tv = TextView as MultiPageTextView;
            if (tv != null)
            {
                tv.OnPageLayoutChanged();
            }
        }
 
        /// <summary>
        /// Calculates the number of columns to fit on one row so that the resultant
        /// view will approximate a "thumbnail" view.
        /// The basic idea is that we attempt to fit (and scale) a number of pages on the row
        /// such that the resultant view given the current Viewport will show as many
        /// pages as possible, with a lower bound of a 5% zoom.
        /// We attempt to optimize to minimize wasted space, where possible.  This may mean
        /// that not all pages will be completely displayed, but we favor a better looking
        /// layout (less dead space) over being able to see every page.
        /// </summary>
        /// <returns>The number of pages to show on the first row.</returns>
        private int CalculateThumbnailColumns()
        {
            //If our current Viewport size is zero, we'll just return 1.
            //(because there always needs to be at least 1 page on a row regardless.)
            if (!IsViewportNonzero)
            {
                return 1;
            }
 
            //If we have no pages, we'll just return 1
            //(because there always needs to be at least 1 page on a row regardless.)
            if (_pageCache.PageCount == 0)
            {
                return 1;
            }
 
            //We use the first page of the document as our basis for our calculations.
            //This means that documents with varying page sizes can potentially have
            //sub-optimal thumbnail views.
            Size pageSize = _pageCache.GetPageSize(0);
 
            //Calculate the viewport's aspect ratio.
            double viewportAspect = ViewportWidth / ViewportHeight;
 
            //Calculate the maximum number of columns we can lay out on a single row
            //without needing to scale below our floor of 12.5%.
            int maxColumns =
                (int)Math.Floor(ViewportWidth /
                    (CurrentMinimumScale * pageSize.Width + HorizontalPageSpacing));
 
            //Ensure this value isn't greater than the number of pages in the document,
            //since we can't possibly lay out a row with more than that number of pages in it.
            maxColumns = Math.Min(maxColumns, _pageCache.PageCount);
            maxColumns = Math.Min(maxColumns, DocumentViewerConstants.MaximumMaxPagesAcross);
 
            //Now we do the following:
            //We iterate through the possible permutations of row and column
            //combinations and choose the arrangement of columns that best fits the current
            //viewport's aspect ratio.
            int minAspectColumns = 1;   //The current optimal number of columns found.
            double minAspectDiff = Double.MaxValue; //The current optimal aspect ratio match
            for (int columns = 1; columns <= maxColumns; columns++)
            {
                //Calculate the number of rows for this arrangment given the current column count
                int rows = (int)Math.Floor((double)(_pageCache.PageCount / columns));
 
                //Calculate the approximate dimensions that this layout would
                //have.
                double width = pageSize.Width * columns;
                double height = pageSize.Height * rows;
 
                //Determine the aspect ratio of this layout.
                double layoutAspect = width / height;
 
                //See if the aspect ratio of this layout is a closer match for our Viewport
                //than previous attempts.
                double aspectDiff = Math.Abs(layoutAspect - viewportAspect);
                if (aspectDiff < minAspectDiff)
                {
                    //It is, so save it.
                    minAspectDiff = aspectDiff;
                    minAspectColumns = columns;
                }
            }
 
            return minAspectColumns;
        }
 
        /// <summary>
        /// Calls InvalidateMeasure() on our visual children in order to force them
        /// to be re-measured and arranged on the next layout pass.  This is called
        /// whenever the Scale is changed, as it is only then when re-measuring is
        /// required.  We do this to avoid unnecessary layout/measure passes on our
        /// pages.
        /// </summary>
        private void InvalidateChildMeasure()
        {
            //Get the current Visual Collection, which contains our pages.
            int count = _childrenCollection.Count;
 
            for (int i = 0; i < count; i++)
            {
                UIElement page = _childrenCollection[i] as UIElement;
 
                if (page != null)
                {
                    page.InvalidateMeasure();
                }
            }
        }
 
        /// <summary>
        /// Helper function that indicates whether all the pages on the specified
        /// row point to clean cache entries.
        /// </summary>
        /// <param name="row"></param>
        /// <returns></returns>
        private bool RowIsClean(RowInfo row)
        {
            bool clean = true;
 
            for (int i = row.FirstPage; i < row.FirstPage + row.PageCount; i++)
            {
                if (_pageCache.IsPageDirty(i))
                {
                    clean = false;
                    break;
                }
            }
 
            return clean;
        }
 
        /// <summary>
        /// Checks that the current scale factor is optimal for the passed in row.
        /// </summary>
        /// <param name="pivotRow">The Row to pass to the Delegate</param>
        private void EnsureFit(RowInfo pivotRow)
        {
            //Get the scale factor necessary to fit this row into view.
            //If the result is not 1.0 (within a certain margin of error)
            //then we need to re-layout, alas.
            double neededScaleFactor = CalculateScaleFactor(pivotRow);
            double newScale = neededScaleFactor * _rowCache.Scale;
 
            //If the neededScaleFactor would require DocumentGrid scale the pages
            //below the minimum allowed zoom, or above the maximum, then we won't
            //do anything here.
            if (newScale < CurrentMinimumScale ||
                newScale > DocumentViewerConstants.MaximumScale)
            {
                return;
            }
 
            if (!DoubleUtil.AreClose(1.0, neededScaleFactor))
            {
                //Rescale the row.
                ApplyViewParameters(pivotRow);
 
                //Make the row visible again -- the offsets may have
                //changed due to the above rescaling.
                SetVerticalOffsetInternal(pivotRow.VerticalOffset);
            }
        }
 
        /// <summary>
        /// Given a pivot row and a previously set ViewMode, the scale is adjusted so as
        /// to cause the pivot row to be fit based on the specified ViewMode.
        /// </summary>
        /// <param name="pivotRow"></param>
        private void ApplyViewParameters(RowInfo pivotRow)
        {
            //Update our MaxPagesAcross property to the number of rows on the pivot row
            //if page sizes vary.  (If page sizes are uniform, this value will not change as a result of
            //a layout change)
            if (_pageCache.DynamicPageSizes)
            {
                _maxPagesAcross = pivotRow.PageCount;
            }
 
            //Get the scale factor necessary to fit the given row into the Viewport.
            double scaleFactor = CalculateScaleFactor(pivotRow);
 
            //Calculate the new scale. We multiply our scale factor by the old scale factor to cancel out any
            //previously applied scale.
            double newScale = scaleFactor * _rowCache.Scale;
 
            //Clip the value into the acceptable range
            newScale = Math.Max(newScale, CurrentMinimumScale);
            newScale = Math.Min(newScale, DocumentViewerConstants.MaximumScale);
 
            //Update the Row Layout's scale.
            UpdateLayoutScale(newScale);
        }
 
        private double CalculateScaleFactor(RowInfo pivotRow)
        {
            //Determine the dimensions of this row minus any spacing between the pages.
            //We use this as the baseline for our scale factor as page spacing does not scale.
            double rowWidth;
 
            //If the page sizes vary, we use the width of the pivot row,
            //otherwise we use the overall width of the document (ExtentWidth).
            //(For uniform page sizes, we always use the width of the document, even
            //for the last row which may not have the same width as the rest of the document).
            if (_pageCache.DynamicPageSizes)
            {
                rowWidth = pivotRow.RowSize.Width - pivotRow.PageCount * HorizontalPageSpacing;
            }
            else
            {
                rowWidth = ExtentWidth - MaxPagesAcross * HorizontalPageSpacing;
            }
 
            double rowHeight = pivotRow.RowSize.Height - VerticalPageSpacing;
 
            //If we have row dimensions of zero or less, there's no reason to scale anything.
            //So just return 1.0 to indicate no change.
            if (rowWidth <= 0.0 || rowHeight <= 0.0)
            {
                return 1.0;
            }
 
            //The dimensions of our Viewport minus any spacing.  We use this as the baseline for our
            //scale factor as page spacing does not scale.
            double compensatedViewportWidth;
 
            if (_pageCache.DynamicPageSizes)
            {
                compensatedViewportWidth = ViewportWidth - pivotRow.PageCount * HorizontalPageSpacing;
            }
            else
            {
                compensatedViewportWidth = ViewportWidth - MaxPagesAcross * HorizontalPageSpacing;
            }
 
            double compensatedViewportHeight = ViewportHeight - VerticalPageSpacing;
 
            //If we have no space to display pages, there's nothing to scale.
            //So just return 1.0 to indicate no change.
            if (compensatedViewportWidth <= 0.0 ||
                compensatedViewportHeight <= 0.0)
            {
                return 1.0;
            }
 
            double scaleFactor = 1.0;
 
            //Based on the previously determined ViewMode (set in SetColumns(), FitToWidth(), etc..
            //scale the pages appropriately.
            switch (_documentLayout.ViewMode)
            {
                case ViewMode.SetColumns:
                    //We leave the scale factor as is -- this is not a "page-fit" mode.
                    break;
 
                case ViewMode.FitColumns:
                    //Update the scale factor so that the pivot row is completely visible.
                    scaleFactor = Math.Min(compensatedViewportWidth / rowWidth, compensatedViewportHeight / rowHeight);
                    break;
 
                case ViewMode.PageWidth:
                    //Update the scale factor so that the pivot row is as wide as the viewport.
                    scaleFactor = compensatedViewportWidth / rowWidth;
                    break;
 
                case ViewMode.PageHeight:
                    //Update the scale factor so that the pivot row is as tall as the viewport.
                    scaleFactor = compensatedViewportHeight / rowHeight;
                    break;
 
                case ViewMode.Thumbnails:
                    //Update the scale factor so that the _entire layout_ is completely visible.  As in previous
                    //cases we must compensate for the fact that the spacing between pages does not scale.
                    //However, unlike in previous cases, we must exclude the space between all rows rather
                    //merely one space, so we must recalculate the compensated values.  Furthermore we must
                    //also compensate for the ExtentHeight as well since it includes the spaces.
                    double thumbnailCompensatedExtentHeight = ExtentHeight - VerticalPageSpacing * _rowCache.RowCount;
                    double thumbnailCompensatedViewportHeight = ViewportHeight - VerticalPageSpacing * _rowCache.RowCount;
                    //If we have no space to display pages, there's nothing to scale.
                    //So just return 1.0 to indicate no change.
                    if (thumbnailCompensatedViewportHeight <= 0.0)
                    {
                        scaleFactor = 1.0;
                    }
                    else
                    {
                        scaleFactor = Math.Min(compensatedViewportWidth / rowWidth,
                            thumbnailCompensatedViewportHeight / thumbnailCompensatedExtentHeight);
                    }
                    break;
 
                case ViewMode.Zoom:
                    //We will not change the scale here, as this is not a "page-fit" mode.
                    break;
 
                default:
                    throw new InvalidOperationException(SR.DocumentGridInvalidViewMode);
            }
 
            return scaleFactor;
        }
 
        /// <summary>
        /// Creates the caches used by DocumentGrid, and sets default property values.
        /// </summary>
        private void Initialize()
        {
            //Create our caches
            _pageCache = new PageCache();
            _childrenCollection = new VisualCollection(this);
 
            _rowCache = new RowCache();
            _rowCache.PageCache = _pageCache;
            _rowCache.RowCacheChanged += new RowCacheChangedEventHandler(OnRowCacheChanged);
            _rowCache.RowLayoutCompleted += new RowLayoutCompletedEventHandler(OnRowLayoutCompleted);
        }
 
        /// <summary>
        /// Updates Scrolling-related properties and calls
        /// InvalidateScrollInfo and InvalidateDocumentScrollInfo on the
        /// ScrollOwner and DocumentScrollOwner, respectively.
        /// </summary>
        private void InvalidateDocumentScrollInfo()
        {
            if (ScrollOwner != null)
            {
                ScrollOwner.InvalidateScrollInfo();
            }
 
            if (DocumentViewerOwner != null)
            {
                DocumentViewerOwner.InvalidateDocumentScrollInfo();
            }
        }
 
        /// <summary>
        /// Calls InvalidatePageViews and ApplyTemplate on our DocumentViewer owner
        /// so that the base implementation can keep its collection up to date.
        /// </summary>
        private void InvalidatePageViews()
        {
            Invariant.Assert(DocumentViewerOwner != null, "DocumentViewerOwner cannot be null.");
 
            if (DocumentViewerOwner != null)
            {
                DocumentViewerOwner.InvalidatePageViewsInternal();
                DocumentViewerOwner.ApplyTemplate();
            }
 
            //Perf Tracing - InvalidatePageViews
            EventTrace.EasyTraceEvent(EventTrace.Keyword.KeywordXPS, EventTrace.Event.WClientDRXInvalidateView);
        }
 
        /// <summary>
        /// Returns an ITextPointer to the current visible selection, if there is one.
        /// </summary>
        /// <returns>An ITextPointer to the current selection, or null if none exists.</returns>
        private ITextPointer GetVisibleSelection()
        {
            ITextPointer selection = null;
 
            if (HasSelection())
            {
                ITextPointer tp = TextEditor.Selection.Start;
 
                //If the TextView contains the selection
                //then the selection is on a visible page.
                if (TextViewContains(tp))
                {
                    selection = tp;
                }
            }
 
            return selection;
        }
 
        /// <summary>
        /// Indicates whether a selection (visible or not) has been made.
        /// </summary>
        /// <returns>true if a selection has been made, false otherwise.</returns>
        private bool HasSelection()
        {
            return (TextEditor != null && TextEditor.Selection != null);
        }
 
        /// <summary>
        /// Gets the page number that the specified ITextPointer to a visible selection
        /// is on.
        /// </summary>
        /// <param name="selection">The TextPointer to find the page number for.</param>
        /// <returns></returns>
        private int GetPageNumberForVisibleSelection(ITextPointer selection)
        {
            Invariant.Assert(TextViewContains(selection));
 
            //Walk through the current DocumentPageViews and see which one contains the selection.
            foreach (DocumentPageView pageView in _pageViews)
            {
                //Get the TextView for this page.
                DocumentPageTextView textView =
                    ((IServiceProvider)pageView).GetService(typeof(ITextView)) as DocumentPageTextView;
 
                //If this TextView contains the selection, return the page's number.
                if (textView != null &&
                    textView.IsValid &&
                    textView.Contains(selection))
                {
                    return pageView.PageNumber;
                }
            }
 
            Invariant.Assert(false, "Selection was in TextView, but not found in any visible page!");
            return 0;
        }
 
        /// <summary>
        /// Finds the "Active Focus" point:
        /// Either the page that has a Selection/Insertion Point on it,
        /// or lacking that, the center of the viewport.
        /// </summary>
        /// <returns></returns>
        private Point GetActiveFocusPoint()
        {
            ITextPointer tp = GetVisibleSelection();
 
            if (tp != null && tp.HasValidLayout)
            {
                Rect selectionRect = TextView.GetRectangleFromTextPosition(tp);
 
                //If the selection rectangle is not empty, then we have a selection or an IP
                if (selectionRect != Rect.Empty)
                {
                    //Return the upper-left corner of the selection.
                    return new Point(selectionRect.Left, selectionRect.Top);
                }
            }
 
            //No selection, so we default to the upper-left of the viewport.
            return new Point(0.0, 0.0);
        }
 
        /// <summary>
        /// Returns the page that has "Active Focus," or
        /// the first visible page if there is none.
        /// </summary>
        /// <returns></returns>
        private int GetActiveFocusPage()
        {
            DocumentPageView dp = GetDocumentPageViewFromPoint(GetActiveFocusPoint());
 
            if (dp != null)
            {
                return dp.PageNumber;
            }
 
            //No selection, we default to the first visible page.
            return _firstVisiblePageNumber;
        }
 
        /// <summary>
        /// Given a point onscreen, returns a DocumentPageView that occupies that point,
        /// if any.
        /// </summary>
        /// <param name="point">The point at which to search for a DocumentPageView.</param>
        /// <returns></returns>
        private DocumentPageView GetDocumentPageViewFromPoint(Point point)
        {
            //Hit test to find the DocumentPageView
            HitTestResult result = VisualTreeHelper.HitTest(this, point);
            DependencyObject currentVisual = (result != null) ? result.VisualHit : null;
 
            DocumentPageView page = null;
 
            // Traverse the visual parent chain until we encounter a DocumentPageView.
            while (currentVisual != null)
            {
                page = currentVisual as DocumentPageView;
                if (page != null)
                {
                    //We found the DocumentPageView.
                    return page;
                }
                currentVisual = VisualTreeHelper.GetParent(currentVisual);
            }
 
            //Didn't find one at this point.
            return null;
        }
 
        /// <summary>
        /// Helper function to safely verify that the TextView contains a given TextPointer.
        /// </summary>
        /// <param name="tp">The TextPointer to check</param>
        /// <returns></returns>
        private bool TextViewContains(ITextPointer tp)
        {
            return (TextView != null &&
                TextView.IsValid &&
                TextView.Contains(tp));
        }
 
        /// <summary>
        /// Helper function that calculates the Horizontal offset of the given page.
        /// </summary>
        /// <param name="row">The row which the desired page lives on</param>
        /// <param name="pageNumber">The page to find the offset for</param>
        /// <returns>The Horizontal offset of the page in the document.</returns>
        private double GetHorizontalOffsetForPage(RowInfo row, int pageNumber)
        {
            ArgumentNullException.ThrowIfNull(row);
            ArgumentOutOfRangeException.ThrowIfLessThan(pageNumber, row.FirstPage);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(pageNumber, row.FirstPage + row.PageCount);
 
            //Rows are centered if the content has varying page sizes,
            //Left-aligned otherwise.
            double horizontalOffset = _pageCache.DynamicPageSizes ?
                Math.Max(0.0, (ExtentWidth - row.RowSize.Width) / 2.0) : 0.0;
 
            //Add the widths of the pages (and spacing) prior to this one on the row
            for (int i = row.FirstPage; i < pageNumber; i++)
            {
                Size pageSize = _pageCache.GetPageSize(i);
                horizontalOffset += pageSize.Width * Scale + HorizontalPageSpacing;
            }
 
            return horizontalOffset;
        }
 
        /// <summary>
        /// Helper method that determines whether a given RowCacheChange will have
        /// an impact on currently-visible rows.
        /// </summary>
        /// <param name="change"></param>
        /// <returns></returns>
        private bool RowCacheChangeIsVisible(RowCacheChange change)
        {
            int firstVisibleRow = _firstVisibleRow;
            int lastVisibleRow = _firstVisibleRow + _visibleRowCount;
 
            int firstChangedRow = change.Start;
            int lastChangedRow = change.Start + change.Count;
 
            //If the first changed row (and hence following changes) are visible OR
            //The last changed row (and hence prior changes) are visible OR
            //if the changes are a super-set of the visible range, then the change is visible.
            if ((firstChangedRow >= firstVisibleRow && firstChangedRow <= lastVisibleRow) ||
                (lastChangedRow >= firstVisibleRow && lastChangedRow <= lastVisibleRow) ||
                (firstChangedRow < firstVisibleRow && lastChangedRow > lastVisibleRow))
            {
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Determines whether the given page's contents have been loaded into the visual tree.
        /// </summary>
        /// <param name="pageNumber">The number of the page to check</param>
        /// <returns></returns>
        private bool IsPageLoaded(int pageNumber)
        {
            DocumentGridPage page = GetDocumentGridPageForPageNumber(pageNumber);
 
            if (page != null)
            {
                return page.IsPageLoaded;
            }
            else
            {
                return false;
            }
        }
 
        /// <summary>
        /// Determines whether every page currently visible has been loaded into the visual tree.
        /// </summary>
        /// <returns></returns>
        private bool IsViewLoaded()
        {
            bool viewIsLoaded = true;
 
            for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++)
            {
                DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage;
 
                // Check that this page has been loaded; break if not.
                if (dp != null && !dp.IsPageLoaded)
                {
                    viewIsLoaded = false;
                    break;
                }
            }
 
            return viewIsLoaded;
        }
 
        /// <summary>
        /// Retrieves a DocumentGridPage from our Visual Tree that has the given page number (if one exists).
        /// </summary>
        /// <param name="pageNumber">The number of the page to get</param>
        /// <returns></returns>
        private DocumentGridPage GetDocumentGridPageForPageNumber(int pageNumber)
        {
            for (int i = _firstPageVisualIndex; i < _childrenCollection.Count; i++)
            {
                DocumentGridPage dp = _childrenCollection[i] as DocumentGridPage;
 
                if (dp != null && dp.PageNumber == pageNumber)
                {
                    return dp;
                }
            }
 
            return null;
        }
 
        #region Event Handlers
 
        /// <summary>
        /// Handles the RequestBringIntoView routed event in the case where the element to be
        /// brought into view is DocumentGrid itself, as is the case when the TextEditor's IP moves.
        /// In this case we use the incoming target rectangle
        /// and ensure that rect is made visible inside of DocumentGrid.
        /// </summary>
        /// <param name="sender">The sender of this routed event, expected to be a DocumentGrid.</param>
        /// <param name="args">The RequestBringIntoView event args associated with this event.</param>
        private static void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs args)
        {
            //We only handle this here if the sender and the target of this event are both the same
            //DocumentGrid.
            DocumentGrid senderGrid = sender as DocumentGrid;
            DocumentGrid targetGrid = args.TargetObject as DocumentGrid;
            if (senderGrid != null && targetGrid != null && senderGrid == targetGrid)
            {
                //Bring the IP into view and mark the event as handled.
                args.Handled = true;
                targetGrid.MakeIPVisible(args.TargetRect);
            }
            else
            {
                args.Handled = false;
            }
        }
 
        /// <summary>
        /// This event is fired when our parent ScrollViewer's layout has changed(before render).
        /// If we need to ensure that a given row has been properly fit -- if ScrollBars have been hidden/
        /// made visible due to this change then we may need to resize.  We call EnsureFit from here
        /// to make sure that's done.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnScrollChanged(object sender, EventArgs args)
        {
            //Remove our handler.
            if (ScrollOwner != null)
            {
                _scrollChangedEventAttached = false;
                ScrollOwner.ScrollChanged -= new ScrollChangedEventHandler(OnScrollChanged);
            }
 
            //Ensure that our fit is good for the currently displayed row if we have any.
            if (_rowCache.HasValidLayout)
            {
                EnsureFit(_rowCache.GetRowForPageNumber(FirstVisiblePageNumber));
            }
        }
 
        /// <summary>
        /// This event is fired after a Zoom change when Layout has completed (but before render).
        /// We make sure the current visible selection is centered onscreen.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnZoomLayoutUpdated(object sender, EventArgs args)
        {
            //Remove the event handler so we don't get called again.
            LayoutUpdated -= new EventHandler(OnZoomLayoutUpdated);
 
            ITextPointer selection = GetVisibleSelection();
 
            if (selection != null)
            {
                //Now we make the selection visible.
                MakeRectVisible(TextView.GetRectangleFromTextPosition(
                    selection), true /* alwaysCenter */);
            }
        }
 
        /// <summary>
        /// When the RowCache is changed for any reason (due to a new layout, or if pages change, etc...)
        /// then we need to invalidate our Measure (so we can pick up any changes to visible pages)
        /// and invalidate our IDocumentScrollInfo parents so they know that something's changed.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="args"></param>
        private void OnRowCacheChanged(object source, RowCacheChangedEventArgs args)
        {
            //If:
            //1) We have a saved pivot row from a previous RowCacheCompleted event,
            //and
            //2) We've been told to do a "Page-Fit" operation (that is, a non-zoom viewing preference)
            //and
            //3) The pivot row is now "clean" (that is, we know the actual dimensions of all the pages
            //   on the row and we aren't just guessing)
            //Then we can now officially calculate the scale needed in order to fit the given row in the manner
            //chosen.
            if (_savedPivotRow != null &&
                RowIsClean(_savedPivotRow))
            {
                if (_documentLayout.ViewMode != ViewMode.Zoom &&
                    _documentLayout.ViewMode != ViewMode.SetColumns
                    )
                {
                    if (_savedPivotRow.FirstPage < _rowCache.RowCount)
                    {
                        RowInfo newRow = _rowCache.GetRowForPageNumber(_savedPivotRow.FirstPage);
 
                        //If the new row's dimensions differ, then we need to rescale, otherwise we do nothing.
                        if (newRow.RowSize.Width != _savedPivotRow.RowSize.Width ||
                            newRow.RowSize.Height != _savedPivotRow.RowSize.Height)
                        {
                            //Rescale.
                            ApplyViewParameters(newRow);
                        }
 
                        //Null out the saved Pivot Row -- we've scaled this row properly now
                        //so we don't need to be concerned with it any longer.
                        _savedPivotRow = null;
                    }
                }
                else
                {
                    // The view is already correct; null out the saved Pivot Row.
                    _savedPivotRow = null;
                }
            }
 
            //If we're viewing a document with varying page size, we've scrolled since the last layout change
            //and the Width of the document has increased
            //then this means that new, wider pages have just been scrolled into view.
            //If we do nothing here, then the content prior to these new pages will appear to "jump"
            //to the right (because we center the pages within the width of the document.)
            //This jump is jarring and not a good user experience.
            //To prevent this, we adjust the HorizontalOffset such that the content that was previously visible
            //appears at the same position when it is rendered.
            if (_pageCache.DynamicPageSizes &&
                _lastRowChangeVerticalOffset != VerticalOffset &&
                _lastRowChangeExtentWidth < ExtentWidth)
            {
                if (_lastRowChangeExtentWidth != 0.0)
                {
                    //Tweak the HorizontalOffset so that the content does not appear to move.
                    SetHorizontalOffsetInternal(HorizontalOffset + (ExtentWidth - _lastRowChangeExtentWidth) / 2.0);
                }
 
                _lastRowChangeExtentWidth = ExtentWidth;
            }
 
            _lastRowChangeVerticalOffset = VerticalOffset;
 
            //The row cache has been changed.
            //If we're displaying rows that were affected,
            //we need to invalidate our measure so they'll be
            //redrawn.
            for (int i = 0; i < args.Changes.Count; i++)
            {
                RowCacheChange change = args.Changes[i];
 
                if (RowCacheChangeIsVisible(change))
                {
                    InvalidateMeasure();
                    InvalidateChildMeasure();
                }
            }
 
            InvalidateDocumentScrollInfo();
        }
 
        /// <summary>
        /// When a new RowLayout has finished being computed we scale the layout such that it
        /// fits within our window.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="args"></param>
        private void OnRowLayoutCompleted(object source, RowLayoutCompletedEventArgs args)
        {
            if (args == null)
            {
                return;
            }
            if (args.PivotRowIndex >= _rowCache.RowCount)
            {
                throw new ArgumentOutOfRangeException("args");
            }
 
            //Get the pivot row
            RowInfo pivotRow = _rowCache.GetRow(args.PivotRowIndex);
 
            //If this row is not clean, and we're not applying a
            //Zoom to the content then we need to rescale the layout when the
            //pages on this row are retrieved.
            if (!RowIsClean(pivotRow) && _documentLayout.ViewMode != ViewMode.Zoom)
            {
                //Save off this row in case we need to rescale due to
                //dirty cache entries becoming clean (i.e. page sizes changing
                //due to the cached size being an inaccurate guess.)
                //OnRowCacheChanged will check this row to ensure that it gets scaled
                //properly when all the pages on the row become available.
                _savedPivotRow = pivotRow;
            }
            else
            {
                _savedPivotRow = null;
            }
 
            //Now rescale.  We do this after checking the cleanliness of the row
            //so that _savedPivotRow is properly set before we apply our view parameters.
            //Otherwise the code that relies on it in OnRowCacheChanged (which may be called
            //as a result of calling ApplyViewParameters) may use the wrong row.
            ApplyViewParameters(pivotRow);
 
            //Now that we've recalculated the row layout, it's time to make the previously-visible
            //content visible again.
            //We do not do this the first time the content is assigned, for two reasons,
            //(similar to the ones described in DocumentViewer.OnDocumentChanged()):
            //  1) If this is the first assignment, then we're already there by default.
            //  2) The user may have specified vertical or horizontal offsets in markup or
            //     otherwise (<DocumentViewer VerticalOffset="1000">) and we need to honor
            //     those settings.
            if (!_firstRowLayout && !_pageJumpAfterLayout)
            {
                MakePageVisible(pivotRow.FirstPage);
            }
            else if (_pageJumpAfterLayout)
            {
                MakePageVisible(_pageJumpAfterLayoutPageNumber);
                _pageJumpAfterLayout = false;
            }
 
            _firstRowLayout = false;
 
            //If our view was of a "Fit" type, we need to ensure that the fit is
            //correct -- if the status of Vertical/Horizontal Scrollbars has changed
            //as a result of our view selection then the Viewport size may have changed.
            //If so, our current fit is probably wrong.  We'll attach a ScrollChanged handler
            //to our ScrollOwner and when the event is invoked (after layout, but before rendering)
            //we'll check.
            //This is "Step 2" of the two-pass layout necessary to do fit properly inside of a
            //ScrollViewer.
            if (!_scrollChangedEventAttached &&
                ScrollOwner != null &&
                _documentLayout.ViewMode != ViewMode.Zoom &&
                _documentLayout.ViewMode != ViewMode.SetColumns)
            {
                _scrollChangedEventAttached = true;
                ScrollOwner.ScrollChanged += new ScrollChangedEventHandler(OnScrollChanged);
            }
        }
 
        #endregion Event Handlers
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Properties
        //
        //------------------------------------------------------
        #region Private Properties
 
        /// <summary>
        /// Indicates that our Viewport is or is not exactly (0,0).
        /// </summary>
        /// <value></value>
        private bool IsViewportNonzero
        {
            get
            {
                return (ViewportWidth != 0.0 && ViewportHeight != 0.0);
            }
        }
 
        /// <summary>
        /// Provides access to DocumentViewer's TextEditor.
        /// </summary>
        private TextEditor TextEditor
        {
            get
            {
                if (DocumentViewerOwner != null)
                {
                    return DocumentViewerOwner.TextEditor;
                }
                else
                {
                    return null;
                }
            }
        }
 
        /// <summary>
        /// Represents the number of pixels to scroll by when using the
        /// Mouse Wheel; based on System.Parameters.WheelScrollLines.
        /// </summary>
        private double MouseWheelVerticalScrollAmount
        {
            get
            {
                //SystemParameters.WheelScrollLines indicates the number of lines to
                //scroll when the wheel is moved one "click," we multiply this by
                //our scroll amount to get the number of pixels to move.
                return _verticalLineScrollAmount * SystemParameters.WheelScrollLines;
            }
        }
 
        private bool CanMouseWheelVerticallyScroll
        {
            get { return _canVerticallyScroll && SystemParameters.WheelScrollLines > 0; }
        }
 
        /// <summary>
        /// Represents the number of pixels to scroll by when using the
        /// Mouse Wheel; based on System.Parameters.WheelScrollLines.
        /// </summary>
        private double MouseWheelHorizontalScrollAmount
        {
            get
            {
                //SystemParameters.WheelScrollLines indicates the number of lines to
                //scroll when the wheel is moved one "click," we multiply this by
                //our scroll amount to get the number of pixels to move.
                return _horizontalLineScrollAmount * SystemParameters.WheelScrollLines;
            }
        }
 
        private bool CanMouseWheelHorizontallyScroll
        {
            get { return _canHorizontallyScroll && SystemParameters.WheelScrollLines > 0; }
        }
 
        /// <summary>
        /// Returns the minimum allowed scale based on the current view mode.
        /// Thumbnails mode has a higher minimum than other views.
        /// </summary>
        private double CurrentMinimumScale
        {
            get
            {
                return _documentLayout.ViewMode == ViewMode.Thumbnails ?
                  DocumentViewerConstants.MinimumThumbnailsScale :
                  DocumentViewerConstants.MinimumScale;
            }
        }
 
        #endregion Private Properties
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        // Our Caches
        private PageCache _pageCache;
        private RowCache _rowCache;
 
        // Our collection of currently-displayed pages.
        private ReadOnlyCollection<DocumentPageView> _pageViews;
 
        // Data for Properties
        private bool _canHorizontallyScroll;
        private bool _canVerticallyScroll;
        private double _verticalOffset;
        private double _horizontalOffset;
        private double _viewportHeight;
        private double _viewportWidth;
        private int _firstVisibleRow;
        private int _visibleRowCount;
        private int _firstVisiblePageNumber;
        private int _lastVisiblePageNumber;
        private ScrollViewer _scrollOwner;
        private DocumentViewer _documentViewerOwner;
        private bool _showPageBorders = true;
        private bool _lockViewModes;
        private int _maxPagesAcross = 1;
 
        // The previous constraint passed to ArrangeCore.
        private Size _previousConstraint;
 
        // The viewing mode (columns, fit, etc...) last used to request a layout.
        private DocumentLayout _documentLayout =
            new DocumentLayout(1, ViewMode.SetColumns);
        private int _documentLayoutsPending;
 
        // The pivot rows last used to form the basis of a layout.
        private RowInfo _savedPivotRow;
 
        // The last ExtentWidth and VerticalOffsets encountered in
        // OnRowCacheChanged, used to determine whether to tweak the HorizontalOffset.
        private double _lastRowChangeExtentWidth;
        private double _lastRowChangeVerticalOffset;
 
        // Editing
        private ITextContainer _textContainer;
 
        // RubberBand selector used for rubberband selection.
        private RubberbandSelector _rubberBandSelector;
 
        // Flags
        private bool _isLayoutRequested;  //Whether we have requested a layout from the RowCache.
        private bool _pageJumpAfterLayout;   //Whether we need to bring a page into view after layout
        private int _pageJumpAfterLayoutPageNumber; //The page to jump to after layout
        private bool _firstRowLayout = true;
        private bool _scrollChangedEventAttached; //Whether or not we've attached a ScrollChanged event to our ScrollViewer.
 
        // We create a Border with a transparent background so that it can
        // participate in Hit-Testing (which allows click events like those
        // for our Context Menu to work).  This border is displayed behind
        // the displayed pages so that "dead space" surrounding the pages can
        // be clicked on.
        private Border _documentGridBackground;
        private const int _backgroundVisualIndex = 0;
        private const int _firstPageVisualIndex = 1;
 
        //Constants for MeasureCore constraints
        //We use this size if we're placed inside a "Size-To-Parent" container like
        //ScrollViewer or StackPanel and are given Infinite constraints.
        private readonly Size _defaultConstraint = new Size(250.0, 250.0);
 
        //Store all our visual children (pages) here
        private VisualCollection _childrenCollection;
 
        //Information for MakeVisible operations involving pages that are not
        //yet visible.
        private int _makeVisiblePageNeeded = -1;
        private DispatcherOperation _makeVisibleDispatcher;
 
        //DispatcherOperations used for executing time-consuming property changes in the background.
        private DispatcherOperation _setScaleOperation;
 
        //Delegate used for BringPageIntoView.
        private delegate void BringPageIntoViewCallback(MakeVisibleData data, int pageNumber);
 
        /// <summary>
        /// Represents a state in the Visual tree merging state machine
        /// used in RecalcVisiblePages.
        /// </summary>
        private enum VisualTreeModificationState
        {
            /// <summary>
            /// Inserting pages before existing
            /// </summary>
            BeforeExisting = 0,
 
            /// <summary>
            /// Scanning through existing pages
            /// </summary>
            DuringExisting,
 
            /// <summary>
            /// Adding pages after existing
            /// </summary>
            AfterExisting
        }
 
        /// <summary>
        /// Represents a layout mode specified by SetColumns,
        /// FitToPage, FitToWidth, etc...
        /// </summary>
        private enum ViewMode
        {
            /// <summary>
            /// A request to lay out a specified number of columns was made.
            /// </summary>
            SetColumns = 0,
 
            /// <summary>
            /// A request to make the specified number of columns visible was made.
            /// </summary>
            FitColumns,
 
            /// <summary>
            /// A request for a fit-to-page-width view was made.
            /// </summary>
            PageWidth,
 
            /// <summary>
            /// A request for a fit-to-page-height view was made.
            /// </summary>
            PageHeight,
 
            /// <summary>
            /// A request for a thumbnail view was made.
            /// </summary>
            Thumbnails,
 
            /// <summary>
            /// A request for a non "page-fit" view was made.
            /// </summary>
            Zoom,
 
            /// <summary>
            /// A request for the HorizontalOffset to be updated.
            /// </summary>
            SetHorizontalOffset,
 
            /// <summary>
            /// A request for the VerticalOffset to be updated.
            /// </summary>
            SetVerticalOffset
        }
 
        /// <summary>
        /// Represents a particular document layout --
        /// includes the number of Columns to view and the
        /// mode to view them in.
        /// </summary>
        private class DocumentLayout
        {
            public DocumentLayout(int columns, ViewMode viewMode)
                : this(columns, 0.0 /* default */, viewMode) { }
 
            public DocumentLayout(double offset, ViewMode viewMode)
                : this(1 /* default */, offset, viewMode) { }
 
            public DocumentLayout(int columns, double offset, ViewMode viewMode)
            {
                _columns = columns;
                _offset = offset;
                _viewMode = viewMode;
            }
 
            /// <summary>
            /// The ViewMode to apply to the layout.
            /// </summary>
            public ViewMode ViewMode
            {
                set { _viewMode = value; }
                get { return _viewMode; }
            }
 
            /// <summary>
            /// The number of columns for the layout.
            /// </summary>
            public int Columns
            {
                set { _columns = value; }
                get { return _columns; }
            }
 
            /// <summary>
            /// The offset (horizontal of vertical) for the layout.
            /// </summary>
            public double Offset
            {
                // Set not currently used.
                // set { _offset = value; }
                get { return _offset; }
            }
 
            private ViewMode _viewMode;
            private int _columns;
            private double _offset;
        }
 
        /// <summary>
        /// An MakeVisibleData object contains data and operation information
        /// related to asynchronous MakeVisible operations.
        /// </summary>
        private struct MakeVisibleData
        {
            /// <summary>
            /// Constructs a new MakeVisibleData object
            /// </summary>
            /// <param name="visual">A visual to be made visible.</param>
            /// <param name="contentPosition">A ContentPosition to be made visible.</param>
            /// <param name="rect">Any bounding rect to be made visible.</param>
            public MakeVisibleData(Visual visual, ContentPosition contentPosition, Rect rect)
            {
                _visual = visual;
                _contentPosition = contentPosition;
                _rect = rect;
            }
 
            /// <summary>
            /// The Visual to be made visible
            /// </summary>
            public Visual Visual
            {
                get { return _visual; }
            }
 
            /// <summary>
            /// The ContentPosition to be made Visible
            /// </summary>
            public ContentPosition ContentPosition
            {
                get { return _contentPosition; }
            }
 
            /// <summary>
            /// The bounding rectangle to be made visible
            /// </summary>
            public Rect Rect
            {
                get { return _rect; }
            }
 
            private Visual _visual;
            private ContentPosition _contentPosition;
            private Rect _rect;
        }
 
        //Constants for line scrolling amounts
        private const double _verticalLineScrollAmount = 16.0;
        private const double _horizontalLineScrollAmount = 16.0;
 
        #endregion Private Fields
    }
}