// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; // IEnumerator using System.Collections.ObjectModel; // ReadOnlyCollection<T> using System.Windows.Annotations; // AnnotationService using System.Windows.Automation.Peers; // AutomationPeer using System.Windows.Documents; // IDocumentPaginatorSource, ... using System.Windows.Documents.Serialization; // WritingCompletedEventArgs using System.Windows.Input; // UICommand using System.Windows.Media; // Visual using System.Windows.Markup; // IAddChild, XamlSerializerUtil, ContentPropertyAttribute using MS.Internal; // Invariant, DoubleUtil using MS.Internal.KnownBoxes; // BooleanBoxes using MS.Internal.Annotations.Anchoring;// DataIdProcessor, FixedPageProcessor, ... using MS.Internal.Documents; // MultiPageTextView using MS.Internal.Controls; // EmptyEnumerator using MS.Internal.Commands; // CommandHelpers namespace System.Windows.Controls.Primitives { /// <summary> /// DocumentViewerBase is a minimal base class, providing only the functionality /// common across document viewing scenarios. The base class provides no user /// interface, very few properties, and minimal policy. Functionality included in /// the base class: BringIntoView support and Print API services /// and Annotation support. /// </summary> [ContentProperty("Document")] public abstract class DocumentViewerBase : Control, IAddChild, IServiceProvider { //------------------------------------------------------------------- // // Constructors // //------------------------------------------------------------------- #region Constructors /// <summary> /// Initializes class-wide settings. /// </summary> static DocumentViewerBase() { // Create our CommandBindings CreateCommandBindings(); // Register event handlers EventManager.RegisterClassHandler(typeof(DocumentViewerBase), RequestBringIntoViewEvent, new RequestBringIntoViewEventHandler(HandleRequestBringIntoView)); // Default value for AutoWordSelection is false. We want true. TextBoxBase.AutoWordSelectionProperty.OverrideMetadata(typeof(DocumentViewerBase), new FrameworkPropertyMetadata(true)); } /// <summary> /// Instantiates a new instance of a DocumentViewerBase. /// </summary> protected DocumentViewerBase() : base() { _pageViews = ReadOnlyCollection<DocumentPageView>.Empty; // By default text selection is enabled. SetFlags(true, Flags.IsSelectionEnabled); } #endregion Constructors //------------------------------------------------------------------- // // Public Methods // //------------------------------------------------------------------- #region Public Methods /// <summary> /// Called when the Template's tree has been generated /// </summary> /// <returns>Whether Visuals were added to the tree.</returns> public override void OnApplyTemplate() { base.OnApplyTemplate(); // Update collection of DocumentPageViews if the Visual tree has // been updated through control template UpdatePageViews(); } /// <summary> /// Moves the master page to the previous page. /// </summary> public void PreviousPage() { OnPreviousPageCommand(); } /// <summary> /// Moves the master page to the next page. /// </summary> public void NextPage() { OnNextPageCommand(); } /// <summary> /// Moves the master page to the first page. /// </summary> public void FirstPage() { OnFirstPageCommand(); } /// <summary> /// Moves the master page to the last page. /// </summary> public void LastPage() { OnLastPageCommand(); } /// <summary> /// Moves the master page to the specified page. /// </summary> /// <param name="pageNumber"></param> public void GoToPage(int pageNumber) { OnGoToPageCommand(pageNumber); } /// <summary> /// Invokes the Print Dialog. This is analogous to the ApplicationCommands.Print. /// </summary> public void Print() { OnPrintCommand(); } /// <summary> /// Cancels current printing job. This is analogous to the ApplicationCommands.CancelPrint. /// </summary> public void CancelPrint() { OnCancelPrintCommand(); } /// <summary> /// Whether the master page can be moved to the specified page. /// </summary> /// <param name="pageNumber">Page number.</param> public virtual bool CanGoToPage(int pageNumber) { // Can navigate to a page, if: // a) the number is in valid range between 1 and PageCount, or // b) Paginator is set and pageNumber is PageCount+1. return (pageNumber > 0 && pageNumber <= this.PageCount) || ((_document != null) && (pageNumber - 1 == this.PageCount) && !_document.DocumentPaginator.IsPageCountValid); } #endregion Public Methods //------------------------------------------------------------------- // // Public Properties // //------------------------------------------------------------------- #region Public Properties /// <summary> /// The IDocumentPaginatorSource to be paginated and viewed. /// </summary> public IDocumentPaginatorSource Document { get { return _document; } set { SetValue(DocumentProperty, value); } } /// <summary> /// The number of pages currently available for viewing. This value /// is updated as content is paginated, and will change dramatically /// when the content is resized, or edited. /// </summary> public int PageCount { get { return (int) GetValue(PageCountProperty); } } /// <summary> /// The one-based page number of the page being displayed on the master /// page DocumentPageView. If there is no content, this value will be 0. /// </summary> public virtual int MasterPageNumber { get { return (int) GetValue(MasterPageNumberProperty); } } /// <summary> /// Whether the viewer can move the master page to the previous page. /// </summary> public virtual bool CanGoToPreviousPage { get { return (bool) GetValue(CanGoToPreviousPageProperty); } } /// <summary> /// Whether the viewer can advance the master page to the next page. /// </summary> public virtual bool CanGoToNextPage { get { return (bool) GetValue(CanGoToNextPageProperty); } } /// <summary> /// A read-only collection of the DocumentPageView objects contained /// within the viewer. These objects are manipulated by the viewer /// in order to display content. /// </summary> [CLSCompliant(false)] public ReadOnlyCollection<DocumentPageView> PageViews { get { return _pageViews; } } #region Public Dynamic Properties /// <summary>\ /// <see cref="Document"/> /// </summary> public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register( "Document", typeof(IDocumentPaginatorSource), typeof(DocumentViewerBase), new FrameworkPropertyMetadata( null, new PropertyChangedCallback(DocumentChanged))); /// <summary> /// <see cref="PageCount"/> /// </summary> protected static readonly DependencyPropertyKey PageCountPropertyKey = DependencyProperty.RegisterReadOnly( "PageCount", typeof(int), typeof(DocumentViewerBase), new FrameworkPropertyMetadata(0)); /// <summary> /// <see cref="PageCount"/> /// </summary> public static readonly DependencyProperty PageCountProperty = PageCountPropertyKey.DependencyProperty; /// <summary> /// <see cref="MasterPageNumber"/> /// </summary> protected static readonly DependencyPropertyKey MasterPageNumberPropertyKey = DependencyProperty.RegisterReadOnly( "MasterPageNumber", typeof(int), typeof(DocumentViewerBase), new FrameworkPropertyMetadata(0)); /// <summary> /// <see cref="MasterPageNumber"/> /// </summary> public static readonly DependencyProperty MasterPageNumberProperty = MasterPageNumberPropertyKey.DependencyProperty; /// <summary> /// <see cref="CanGoToPreviousPage"/> /// </summary> protected static readonly DependencyPropertyKey CanGoToPreviousPagePropertyKey = DependencyProperty.RegisterReadOnly( "CanGoToPreviousPage", typeof(bool), typeof(DocumentViewerBase), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox)); /// <summary> /// <see cref="CanGoToPreviousPage"/> /// </summary> public static readonly DependencyProperty CanGoToPreviousPageProperty = CanGoToPreviousPagePropertyKey.DependencyProperty; /// <summary> /// <see cref="CanGoToNextPage"/> /// </summary> protected static readonly DependencyPropertyKey CanGoToNextPagePropertyKey = DependencyProperty.RegisterReadOnly( "CanGoToNextPage", typeof(bool), typeof(DocumentViewerBase), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox)); /// <summary> /// <see cref="CanGoToNextPage"/> /// </summary> public static readonly DependencyProperty CanGoToNextPageProperty = CanGoToNextPagePropertyKey.DependencyProperty; /// <summary> /// Attached property used to signify which of the child DocumentPageView objects /// is the master page. If more than one DocumentPageView has this value set, /// then the first tagged DocumentPageView in the tree (depth-first) is designated /// the master. If none of the children have this property set, then the depth-first /// PageView is designated the master. /// </summary> public static readonly DependencyProperty IsMasterPageProperty = DependencyProperty.RegisterAttached( "IsMasterPage", typeof(bool), typeof(DocumentViewerBase), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox)); /// <summary> /// DependencyProperty getter for <see cref="IsMasterPageProperty" /> property. /// </summary> /// <param name="element">The element from which to read the attached property.</param> public static bool GetIsMasterPage(DependencyObject element) { ArgumentNullException.ThrowIfNull(element); return (bool)element.GetValue(IsMasterPageProperty); } /// <summary> /// DependencyProperty setter for <see cref="IsMasterPageProperty" /> property. /// </summary> /// <param name="element">The element to which to write the attached property.</param> /// <param name="value">The property value to set</param> public static void SetIsMasterPage(DependencyObject element, bool value) { ArgumentNullException.ThrowIfNull(element); element.SetValue(IsMasterPageProperty, value); } #endregion Public Dynamic Properties #endregion Public Properties //------------------------------------------------------------------- // // Public Events // //------------------------------------------------------------------- #region Public Events /// <summary> /// Fired when collection of DocumentPageViews is changed. /// </summary> public event EventHandler PageViewsChanged; #endregion Public Events //------------------------------------------------------------------- // // Protected Methods // //------------------------------------------------------------------- #region Protected Methods /// <summary> /// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>) /// </summary> protected override AutomationPeer OnCreateAutomationPeer() { return new DocumentViewerBaseAutomationPeer(this); } protected override void OnDpiChanged(DpiScale oldDpiScaleInfo, DpiScale newDpiScaleInfo) { FlowDocument flowDocument = _document as FlowDocument; flowDocument?.SetDpi(newDpiScaleInfo); } /// <summary> /// Invalidates the PageViews collection, triggering a call to GetPageViews. /// </summary> protected void InvalidatePageViews() { // Update collection of DocumentPageViews if the collection has // been explicitly invalidated. UpdatePageViews(); InvalidateMeasure(); } /// <summary> /// Returns the master DocumentPageView. /// </summary> protected DocumentPageView GetMasterPageView() { int index; DocumentPageView masterPageView = null; // Search for the first element with IsMasterPage property set. for (index = 0; index < _pageViews.Count; index++) { if (GetIsMasterPage(_pageViews[index])) { masterPageView = _pageViews[index]; break; } } // If none of the DocumentPageViews have this property set, // then use the first one in the collection. if (masterPageView == null) { masterPageView = _pageViews.Count > 0 ? _pageViews[0] : null; } return masterPageView; } /// <summary> /// Creates a collection of DocumentPageView objects used to display Document. /// </summary> /// <param name="changed">True if the collection is different than the public PageViews collection.</param> /// <returns>Collection of DocumentPageView objects used to display Document.</returns> protected virtual ReadOnlyCollection<DocumentPageView> GetPageViewsCollection(out bool changed) { List<DocumentPageView> pageViewList; AdornerDecorator adornerDecorator; // By default retrieve all DocumentPageViews from style. pageViewList = new List<DocumentPageView>(1/* simplest case has just one element */); FindDocumentPageViews(this, pageViewList); // By default use AdornerDecorator.Child as RenderScope for TextEditor. Retieve // AdornerDecorator from the style. adornerDecorator = FindAdornerDecorator(this); this.TextEditorRenderScope = (adornerDecorator != null) ? (adornerDecorator.Child as FrameworkElement) : null; // Since existing DocumentPageViews are being replaced, need to disconnect // them from the Document. for (int index = 0; index < _pageViews.Count; index++) { _pageViews[index].DocumentPaginator = null; } changed = true; return new ReadOnlyCollection<DocumentPageView>(pageViewList); } /// <summary> /// Called when the PageViews collection is modified; this occurs when GetPageViews /// returns True or if the control's template is modified. /// </summary> protected virtual void OnPageViewsChanged() { // Raise notification about change to DocumentPageView collection. if (this.PageViewsChanged != null) { this.PageViewsChanged(this, EventArgs.Empty); } // Change of DocumentPageView collection may cause invalidation of content // represented by DocumentPageViews. OnMasterPageNumberChanged(); } /// <summary> /// Called when the MasterPageNumber property is changed, this occurs when the /// developer manually sets the property, or when the FirstPage, NextPage, /// etc commands are executed. /// </summary> protected virtual void OnMasterPageNumberChanged() { // Invalidation of MasterPage invalidates following properties: // - MasterPageNumber // - CanGoToPreviousPage // - CanGoToNextPage UpdateReadOnlyProperties(true, true); } /// <summary> /// Called when a BringIntoView event is bubbled up from the Document. /// Base implementation will move the master page to the page /// on which the element occurs. /// </summary> /// <param name="element">The object to make visible.</param> /// <param name="rect">The rectangular region in the object's coordinate space which should be made visible.</param> /// <param name="pageNumber"></param> protected virtual void OnBringIntoView(DependencyObject element, Rect rect, int pageNumber) { ArgumentNullException.ThrowIfNull(element); OnGoToPageCommand(pageNumber); } /// <summary> /// Handler for the PreviousPage command. /// </summary> protected virtual void OnPreviousPageCommand() { // If can go to the previous page, shift all pages // by decrementing page number by 1. if (this.CanGoToPreviousPage) { ShiftPagesByOffset(-1); } } /// <summary> /// Handler for the NextPage command. /// </summary> protected virtual void OnNextPageCommand() { // If can go to the next page, shift all pages // by incrementing page number by 1. if (this.CanGoToNextPage) { ShiftPagesByOffset(1); } } /// <summary> /// Handler for the FirstPage command. /// </summary> protected virtual void OnFirstPageCommand() { // Navigate the master page to the first one and shift // all remaining pages by delta. ShiftPagesByOffset(1 - this.MasterPageNumber); } /// <summary> /// Handler for the LastPage command. /// </summary> protected virtual void OnLastPageCommand() { // Navigate the master page to the last one and shift // all remaining pages by delta. ShiftPagesByOffset(this.PageCount - this.MasterPageNumber); } /// <summary> /// Handler for the GoToPage command. /// </summary> /// <param name="pageNumber"></param> protected virtual void OnGoToPageCommand(int pageNumber) { // Check if can go to the specified page. // Navigate the master page to specified page number and shift // all remaining pages by delta. if (CanGoToPage(pageNumber)) { ShiftPagesByOffset(pageNumber - this.MasterPageNumber); } } /// <summary> /// Handler for the Print command. /// </summary> protected virtual void OnPrintCommand() { #if !DONOTREFPRINTINGASMMETA System.Windows.Xps.XpsDocumentWriter docWriter; System.Printing.PrintDocumentImageableArea ia = null; // Only one printing job is allowed. if (_documentWriter != null) { return; } if (_document != null) { // Show print dialog. docWriter = System.Printing.PrintQueue.CreateXpsDocumentWriter(ref ia); if (docWriter != null && ia != null) { // Register for WritingCompleted event. _documentWriter = docWriter; _documentWriter.WritingCompleted += new WritingCompletedEventHandler(HandlePrintCompleted); _documentWriter.WritingCancelled += new WritingCancelledEventHandler(HandlePrintCancelled); // Since _documentWriter value is used to determine CanExecute state, we must invalidate that state. CommandManager.InvalidateRequerySuggested(); // Write to the PrintQueue if( _document is FixedDocumentSequence ) { docWriter.WriteAsync(_document as FixedDocumentSequence); } else if( _document is FixedDocument ) { docWriter.WriteAsync(_document as FixedDocument); } else { docWriter.WriteAsync(_document.DocumentPaginator); } } } #endif // DONOTREFPRINTINGASMMETA } /// <summary> /// Handler for the CancelPrint command. /// </summary> protected virtual void OnCancelPrintCommand() { #if !DONOTREFPRINTINGASMMETA _documentWriter?.CancelAsync(); #endif // DONOTREFPRINTINGASMMETA } /// <summary> /// Called when the Document property is changed. /// </summary> protected virtual void OnDocumentChanged() { int index; // Document has been changed. Update existing DocumentPageViews to point them to the new Document. for (index = 0; index < _pageViews.Count; index++) { _pageViews[index].DocumentPaginator = _document?.DocumentPaginator; } // Document invalidation invalidates following properties: // - PageCount // - MasterPageNumber // - CanGoToPreviousPage // - CanGoToNextPage UpdateReadOnlyProperties(true, true); // Attach TextEditor, if content supports it. This method will also // detach TextEditor from old content. AttachTextEditor(); } #endregion Protected Methods //------------------------------------------------------------------- // // Protected Properties // //------------------------------------------------------------------- #region Protected Properties /// <summary> /// Returns enumerator to logical children. /// </summary> protected internal override IEnumerator LogicalChildren { get { if (this.HasLogicalChildren && _document != null) { return new SingleChildEnumerator(_document); } return EmptyEnumerator.Instance; } } #endregion Protected Properties //------------------------------------------------------------------- // // Internal Methods // //------------------------------------------------------------------- #region Internal Methods /// <summary> /// Determines whether DocumentPageView is a master page. /// </summary> /// <param name="pageView">Instance of DocumentPageView.</param> /// <returns>Whether given instance of DocumentPageView is a master page.</returns> internal bool IsMasterPageView(DocumentPageView pageView) { Invariant.Assert(pageView != null); return (pageView == GetMasterPageView()); } /// <summary> /// Invoked when the "Find" button in the Find Toolbar is clicked. /// This method invokes the actual Find process. /// </summary> internal ITextRange Find(FindToolBar findToolBar) { ITextView masterPageTextView = null; DocumentPageView masterPage = GetMasterPageView(); if (masterPage != null && masterPage is IServiceProvider) { masterPageTextView = ((IServiceProvider)masterPage).GetService(typeof(ITextView)) as ITextView; } return DocumentViewerHelper.Find(findToolBar, _textEditor, _textView, masterPageTextView); } #endregion Internal Methods //------------------------------------------------------------------- // // Internal Properties // //------------------------------------------------------------------- #region Internal Properties /// <summary> /// Whether text selection is enabled or disabled. /// </summary> internal bool IsSelectionEnabled { get { return CheckFlags(Flags.IsSelectionEnabled); } set { SetFlags(value, Flags.IsSelectionEnabled); AttachTextEditor(); } } /// <summary> /// TextEditor instance. /// </summary> internal TextEditor TextEditor { get { return _textEditor; } } /// <summary> /// Allows overriding the RenderScope used by the TextEditor. /// </summary> internal FrameworkElement TextEditorRenderScope { get { return _textEditorRenderScope; } set { _textEditorRenderScope = value; AttachTextEditor(); } } #endregion Internal Properties //------------------------------------------------------------------- // // Private Methods // //------------------------------------------------------------------- #region Private Methods /// <summary> /// Retrieves an ITextPointer from the MasterPage. /// If startOfPage is true, then we will retreive the first /// TextPointer in the first TextSegment in the DocumentPageTextView. /// Else, we will retreive the last TextPointer in the last TextSegment /// of the MasterPage DocumentPageTextView. /// </summary> /// <param name="startOfPage"></param> /// <returns></returns> private ITextPointer GetMasterPageTextPointer(bool startOfPage) { ITextPointer masterPointer = null; ITextView textView = null; DocumentPageView masterPage = GetMasterPageView(); if (masterPage != null && masterPage is IServiceProvider) { textView = ((IServiceProvider)masterPage).GetService(typeof(ITextView)) as ITextView; if (textView != null && textView.IsValid) { // Find the very first/(last) text pointer in this textView. foreach (TextSegment textSegment in textView.TextSegments) { if (textSegment.IsNull) { continue; } if (masterPointer == null) { // Set initial masterPointer value. masterPointer = startOfPage ? textSegment.Start : textSegment.End; } else { if (startOfPage) { if (textSegment.Start.CompareTo(masterPointer) < 0) { // Start is before the current masterPointer masterPointer = textSegment.Start; } } else { // end is after than the current masterPointer if (textSegment.End.CompareTo(masterPointer) > 0) { masterPointer = textSegment.End; } } } } } } return masterPointer; } /// <summary> /// Update collection of DocumentPageViews in responce to Visual tree changes. /// </summary> /// <returns>Whether collection of DocumentPageViews has been updated.</returns> private void UpdatePageViews() { int index; bool changed; ReadOnlyCollection<DocumentPageView> pageViews; // Get collection of new DocumentPageViews. pageViews = GetPageViewsCollection(out changed); // If DocumentPageViews collection has not been changed, there is nothing to update. if (changed) { // Verify collection of DocumentPageViews. It needs to meet following conditions: // a) at least one DocumentPageView, // b) only one page has IsMasterPage property set to true, // c) unique PageNumbers for each active DocumentPageView, VerifyDocumentPageViews(pageViews); // New collection of DocumentPageViews is replacing the old one. Point new // DocumentPageViews to the Document. _pageViews = pageViews; for (index = 0; index < _pageViews.Count; index++) { _pageViews[index].DocumentPaginator = _document?.DocumentPaginator; } // Collection of DocumentPageView has been changed. Need to update // TextView, if one already exists. _textView?.OnPagesUpdated(); // DocumentPageViews collection has been changed. Notify all listeners // and/or derived classes about this fact. OnPageViewsChanged(); } } /// <summary> /// Verify collection of DocumentPageViews. It needs to meet following conditions: /// a) collection is not null, /// b) only one page has IsMasterPage property set to true, /// c) unique PageNumbers for each active DocumentPageView, /// </summary> /// <param name="pageViews">Collection of DocumentPageViews to validate.</param> private void VerifyDocumentPageViews(ReadOnlyCollection<DocumentPageView> pageViews) { int index; bool hasMasterPage = false; // At least one DocumentPageView is required. if (pageViews == null) { throw new ArgumentException(SR.DocumentViewerPageViewsCollectionEmpty); } // Expecting only one DocumentPageView with IsMasterPage property set to true. for (index = 0; index < pageViews.Count; index++) { if (GetIsMasterPage(pageViews[index])) { if (hasMasterPage) { throw new ArgumentException(SR.DocumentViewerOneMasterPage); } hasMasterPage = true; } } // Unique PageNumbers for each active DocumentPageView. } /// <summary> /// Does deep Visual tree walk to retrieve all DocumentPageViews. /// It stops recursing down into visual tree in following situations: /// a) Visual is UIElement and it is not part of Contol Template, /// b) Visual is DocumentPageView. /// </summary> /// <param name="root">FrameworkElement that is part of Control Template.</param> /// <param name="pageViews">Collection of DocumentPageViews; found elements are appended here.</param> /// <returns>Whether collection of DocumentPageViews has been updated.</returns> private void FindDocumentPageViews(Visual root, List<DocumentPageView> pageViews) { Invariant.Assert(root != null); Invariant.Assert(pageViews != null); FrameworkElement fe; // Do deep tree walk to retrieve all DocumentPageViews. // It stops recursing down into visual tree in following situations: // a) Visual is UIElement and it is not part of Contol Template, // b) Visual is DocumentPageView. // Add to collection any DocumentPageViews found in the Control Template. int count = root.InternalVisualChildrenCount; for(int i = 0; i < count; i++) { Visual child = root.InternalGetVisualChild(i); fe = child as FrameworkElement; if (fe != null) { if (fe.TemplatedParent != null) { if (fe is DocumentPageView) { pageViews.Add(fe as DocumentPageView); } else { FindDocumentPageViews(fe, pageViews); } } } else { FindDocumentPageViews(child, pageViews); } } } /// <summary> /// Does deep Visual tree walk to retrieve an AdornerDecorator. Because /// AdornerDecorator is supposed to cover all DocumentPageViews, it stops /// recursing down into visual tree in following situations: /// a) Visual is UIElement and it is not part of Contol Template, /// b) Visual is DocumentPageView. /// c) Visual is AdornerDecorator. /// </summary> /// <param name="root">FrameworkElement that is part of Control Template.</param> /// <returns>AdornerDecorator, if found.</returns> private AdornerDecorator FindAdornerDecorator(Visual root) { Invariant.Assert(root != null); FrameworkElement fe; AdornerDecorator adornerDecorator = null; // Do deep Visual tree walk to retrieve an AdornerDecorator. Because // AdornerDecorator is supposed to cover all DocumentPageViews, it stops // recursing down into visual tree in following situations: // a) Visual is UIElement and it is not part of Contol Template, // b) Visual is DocumentPageView. // c) Visual is AdornerDecorator. int count = root.InternalVisualChildrenCount; for(int i = 0; i < count; i++) { Visual child = root.InternalGetVisualChild(i); fe = child as FrameworkElement; if (fe != null) { if (fe.TemplatedParent != null) { if (fe is AdornerDecorator) { adornerDecorator = (AdornerDecorator)fe; } else if (!(fe is DocumentPageView)) { adornerDecorator = FindAdornerDecorator(fe); } // else stop on DocumentPageView } } else { adornerDecorator = FindAdornerDecorator(child); } if (adornerDecorator != null) { break; } } return adornerDecorator; } /// <summary> /// Attach TextEditor to Document, if supports text. /// </summary> private void AttachTextEditor() { AnnotationService service = AnnotationService.GetService(this); ITextContainer textContainer; // This method is called when Document is changing, so need // to clear old TextEditor data. if (_textEditor != null) { _textEditor.OnDetach(); _textEditor = null; if (_textView.TextContainer.TextView == _textView) { _textView.TextContainer.TextView = null; } _textView = null; } // Must be enabled - otherwise it won't be on the tree service?.Disable(); // If new Document supports TextEditor, create one. // If the Document is already attached to TextEditor (TextSelection != null), // do not create TextEditor for this instance of the viewer. (This situation may happen // when the same instance of Document is attached to more than one viewer). textContainer = this.TextContainer; if (textContainer != null && this.TextEditorRenderScope != null && textContainer.TextSelection == null) { _textView = new MultiPageTextView(this, this.TextEditorRenderScope, textContainer); _textEditor = new TextEditor(textContainer, this, false) { IsReadOnly = !IsEditingEnabled, TextView = _textView }; textContainer.TextView = _textView; } // Re-enable the service in order to register on the new TextView service?.Enable(service.Store); } /// <summary> /// Called when WritingCompleted event raised by a DocumentWriter (during printing). /// </summary> /// <param name="sender">Sender of the event.</param> /// <param name="e">Event arguments.</param> private void HandlePrintCompleted(object sender, WritingCompletedEventArgs e) { CleanUpPrintOperation(); } /// <summary> /// Called when WritingCancelled event raised by a DocumentWriter (during printing). /// </summary> /// <param name="sender">Sender of the event.</param> /// <param name="e">Event arguments.</param> private void HandlePrintCancelled(object sender, WritingCancelledEventArgs e) { CleanUpPrintOperation(); } /// <summary> /// Handler for PaginationCompleted event raised by the Document. /// </summary> private void HandlePaginationCompleted(object sender, EventArgs e) { // PaginationCompleted may invalidate following properties: // - PageCount // - CanGoToNextPage UpdateReadOnlyProperties(true, false); } /// <summary> /// Handler for PaginationProgress event raised by the Document. /// </summary> private void HandlePaginationProgress(object sender, EventArgs e) { // PaginationProgress may invalidate following properties: // - PageCount // - CanGoToNextPage UpdateReadOnlyProperties(true, false); } /// <summary> /// Handler for GetPageNumberCompleted event raised by the Document. /// </summary> private void HandleGetPageNumberCompleted(object sender, GetPageNumberCompletedEventArgs e) { BringIntoViewState bringIntoViewState; // At this point the Document's page count might have changed, // so update properties accordingly. UpdateReadOnlyProperties(true, false); if (_document != null && sender == _document.DocumentPaginator && e != null) { if (!e.Cancelled && (e.Error == null)) { bringIntoViewState = e.UserState as BringIntoViewState; if (bringIntoViewState != null && bringIntoViewState.Source == this) { OnBringIntoView(bringIntoViewState.TargetObject, bringIntoViewState.TargetRect, e.PageNumber + 1); } } } } /// <summary> /// Makes sure the target is visible in the client area. May cause navigation /// to a different page. /// </summary> /// <param name="args">RequestBringIntoViewEventArgs indicates the element and region to scroll into view.</param> private void HandleRequestBringIntoView(RequestBringIntoViewEventArgs args) { DependencyObject child; DependencyObject parent; ContentPosition contentPosition; BringIntoViewState bringIntoViewState; DynamicDocumentPaginator documentPaginator; Rect targetRect = Rect.Empty; if (args != null && args.TargetObject != null && _document is DependencyObject) { // If the passed in object is a logical child of DocumentViewer's Document, // attempt to make it visible now. // Special case: TargetObject is the document itself. Then scroll to the top (page 1). // This supports navigating from baseURI#anchor to just baseURI. parent = _document as DependencyObject; if (args.TargetObject == _document) { OnGoToPageCommand(1); args.Handled = true; // Mark the event as handled. } else { // Verify if TargetObject is in fact a child of Document. child = args.TargetObject; while (child != null && child != parent) { // Skip elements in the control's template (if such exists) and // walk up logical tree to find if the focused element is within // the document. FrameworkElement fe = child as FrameworkElement; if (fe != null && fe.TemplatedParent != null) { child = fe.TemplatedParent; } else { child = LogicalTreeHelper.GetParent(child); } } if (child != null) { // Special case UIElements already connected to visual tree. if (args.TargetObject is UIElement targetObject) { if (VisualTreeHelper.IsAncestorOf(this, targetObject)) { targetRect = args.TargetRect; if (targetRect.IsEmpty) { targetRect = new Rect(targetObject.RenderSize); } GeneralTransform transform = targetObject.TransformToAncestor(this); targetRect = transform.TransformBounds(targetRect); targetRect.IntersectsWith(new Rect(this.RenderSize)); } } // If target is not already visible, bring appropriate page into view. if (targetRect.IsEmpty) { // Get content position for given target. documentPaginator = _document.DocumentPaginator as DynamicDocumentPaginator; if (documentPaginator != null) { contentPosition = documentPaginator.GetObjectPosition(args.TargetObject); if (contentPosition != null && contentPosition != ContentPosition.Missing) { // Asynchronously retrieve PageNumber for given ContentPosition. bringIntoViewState = new BringIntoViewState(this, contentPosition, args.TargetObject, args.TargetRect); documentPaginator.GetPageNumberAsync(contentPosition, bringIntoViewState); } } } args.Handled = true; // Mark the event as handled. } } if (args.Handled) { // Create new BringIntoView request for this element, so // if there is an ancestor handling BringIntoView, it can // react appropriately and bring this element into view. if (targetRect.IsEmpty) { BringIntoView(); } else { BringIntoView(targetRect); } } } } /// <summary> /// Update values for readonly properties. /// </summary> /// <param name="pageCountChanged">Whether PageCount has been changed.</param> /// <param name="masterPageChanged">Whether MasterPageNumber has been changed.</param> private void UpdateReadOnlyProperties(bool pageCountChanged, bool masterPageChanged) { if (pageCountChanged) { SetValue(PageCountPropertyKey, (_document != null) ? _document.DocumentPaginator.PageCount : 0); } bool invalidateRequery = false; if (masterPageChanged) { int masterPageNumber = 0; DocumentPageView masterPageView; if (_document != null && _pageViews.Count > 0) { masterPageView = GetMasterPageView(); if (masterPageView != null) { masterPageNumber = masterPageView.PageNumber + 1; } } SetValue(MasterPageNumberPropertyKey, masterPageNumber); SetValue(CanGoToPreviousPagePropertyKey, MasterPageNumber > 1); invalidateRequery = true; } if (pageCountChanged || masterPageChanged) { bool canGoToNextPage = false; if (_document != null) { canGoToNextPage = (MasterPageNumber < _document.DocumentPaginator.PageCount) || !_document.DocumentPaginator.IsPageCountValid; } SetValue(CanGoToNextPagePropertyKey, canGoToNextPage); invalidateRequery = true; } if(invalidateRequery) { CommandManager.InvalidateRequerySuggested(); } } /// <summary> /// Shift all pages by specified offset. /// </summary> private void ShiftPagesByOffset(int offset) { int index; if (offset != 0) { for (index = 0; index < _pageViews.Count; index++) { _pageViews[index].PageNumber += offset; } // Change of PageNumber property on DocumentPageViews will cause // invalidation of content represented by DocumentPageViews. OnMasterPageNumberChanged(); } } /// <summary> /// Sets or unsets one or multiple flags. /// </summary> /// <param name="value">Whether flag or flags are set or cleared.</param> /// <param name="flags">Combination of flags to change.</param> private void SetFlags(bool value, Flags flags) { _flags = value ? (_flags | flags) : (_flags & (~flags)); } /// <summary> /// Returns true if all of passed flags in the bitmask are set. /// </summary> /// <param name="flags">Combination of flags to get the value.</param> /// <returns>Returns true if all of passed flags in the bitmask are set.</returns> private bool CheckFlags(Flags flags) { return ((_flags & flags) == flags); } /// <summary> /// The Document has changed and needs to be updated. /// </summary> private void DocumentChanged(IDocumentPaginatorSource oldDocument, IDocumentPaginatorSource newDocument) { DependencyObject doDocument; DynamicDocumentPaginator dynamicDocumentPaginator; _document = newDocument; // Cleanup state associated with the old document. if (oldDocument != null) { // If Document was added to logical tree of DocumentViewer before, remove it. if (CheckFlags(Flags.DocumentAsLogicalChild)) { RemoveLogicalChild(oldDocument); } // Unregister from PaginationProgress and PaginationCompleted events. dynamicDocumentPaginator = oldDocument.DocumentPaginator as DynamicDocumentPaginator; if (dynamicDocumentPaginator != null) { dynamicDocumentPaginator.PaginationProgress -= new PaginationProgressEventHandler(HandlePaginationProgress); dynamicDocumentPaginator.PaginationCompleted -= new EventHandler(HandlePaginationCompleted); dynamicDocumentPaginator.GetPageNumberCompleted -= new GetPageNumberCompletedEventHandler(HandleGetPageNumberCompleted); } DependencyObject depObj = oldDocument as DependencyObject; depObj?.ClearValue(PathNode.HiddenParentProperty); } // If DocumentViewer was created through style, then do not modify // the logical tree. Instead, set "core parent" for the Document. doDocument = _document as DependencyObject; if (doDocument != null && LogicalTreeHelper.GetParent(doDocument) != null && doDocument is ContentElement) { // Set the "core parent" back to us. ContentOperations.SetParent((ContentElement)doDocument, this); SetFlags(false, Flags.DocumentAsLogicalChild); } else { SetFlags(true, Flags.DocumentAsLogicalChild); } // Initialize state associated with the new document. if (_document != null) { // If Document should be part of DocumentViewer's logical tree, add it. if (CheckFlags(Flags.DocumentAsLogicalChild)) { AddLogicalChild(_document); } // Register for PaginationProgress and PaginationCompleted events. dynamicDocumentPaginator = _document.DocumentPaginator as DynamicDocumentPaginator; if (dynamicDocumentPaginator != null) { dynamicDocumentPaginator.PaginationProgress += new PaginationProgressEventHandler(HandlePaginationProgress); dynamicDocumentPaginator.PaginationCompleted += new EventHandler(HandlePaginationCompleted); dynamicDocumentPaginator.GetPageNumberCompleted += new GetPageNumberCompletedEventHandler(HandleGetPageNumberCompleted); } // Setup DPs and processors for annotation handling. If the service isn't already // enabled the processors will be registered by the service when it is enabled. FlowDocument flowDocument; DependencyObject doc = _document as DependencyObject; if (_document is FixedDocument || _document is FixedDocumentSequence) { // Clear properties that aren't needed for FixedDocument this.ClearValue(AnnotationService.DataIdProperty); // Setup service to look for FixedPages in the content AnnotationService.SetSubTreeProcessorId(this, FixedPageProcessor.Id); // Tell the content how to get to its parent DocumentViewer doc.SetValue(PathNode.HiddenParentProperty, this); // If the service is already registered, set it up for fixed content AnnotationService service = AnnotationService.GetService(this); if (service != null) { service.LocatorManager.RegisterSelectionProcessor(new FixedTextSelectionProcessor(), typeof(TextRange)); service.LocatorManager.RegisterSelectionProcessor(new FixedTextSelectionProcessor(), typeof(TextAnchor)); } } else if ((flowDocument = _document as FlowDocument) != null) { flowDocument.SetDpi(this.GetDpi()); // Tell the content how to get to its parent DocumentViewer flowDocument.SetValue(PathNode.HiddenParentProperty, this); // If the service is already registered, set it up for fixed content AnnotationService service = AnnotationService.GetService(this); if (service != null) { service.LocatorManager.RegisterSelectionProcessor(new TextSelectionProcessor(), typeof(TextRange)); service.LocatorManager.RegisterSelectionProcessor(new TextSelectionProcessor(), typeof(TextAnchor)); service.LocatorManager.RegisterSelectionProcessor(new TextViewSelectionProcessor(), typeof(DocumentViewerBase)); } // Setup service to use DataID processor AnnotationService.SetDataId(this, "FlowDocument"); } else { // Clear values that were set directly on the tree - only valid for Fixed or Flow Documents this.ClearValue(AnnotationService.SubTreeProcessorIdProperty); this.ClearValue(AnnotationService.DataIdProperty); } } // Document is also represented as Automation child. Need to invalidate peer to force update. DocumentViewerBaseAutomationPeer peer = UIElementAutomationPeer.FromElement(this) as DocumentViewerBaseAutomationPeer; peer?.InvalidatePeer(); // Respond to Document change - update state that is affected by this change. OnDocumentChanged(); } /// <summary> /// Cleans up after a print operation by unregistering for events and re-enabling buttons. /// </summary> private void CleanUpPrintOperation() { #if !DONOTREFPRINTINGASMMETA if (_documentWriter != null) { _documentWriter.WritingCompleted -= new WritingCompletedEventHandler(HandlePrintCompleted); _documentWriter.WritingCancelled -= new WritingCancelledEventHandler(HandlePrintCancelled); _documentWriter = null; // Since _documentWriter value is used to determine CanExecute state, we must invalidate that state. CommandManager.InvalidateRequerySuggested(); } #endif // DONOTREFPRINTINGASMMETA } #region Commands /// <summary> /// Set up Command and RoutedCommand bindings. /// </summary> private static void CreateCommandBindings() { ExecutedRoutedEventHandler executedHandler; CanExecuteRoutedEventHandler canExecuteHandler; // Create our generic ExecutedRoutedEventHandler. executedHandler = new ExecutedRoutedEventHandler(ExecutedRoutedEventHandler); // Create our generic QueryEnabledStatusHandler canExecuteHandler = new CanExecuteRoutedEventHandler(CanExecuteRoutedEventHandler); // Command: NavigationCommands.PreviousPage CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), NavigationCommands.PreviousPage, executedHandler, canExecuteHandler); // no key gesture // Command: NavigationCommands.NextPage CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), NavigationCommands.NextPage, executedHandler, canExecuteHandler); // no key gesture // Command: NavigationCommands.FirstPage CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), NavigationCommands.FirstPage, executedHandler, canExecuteHandler); // no key gesture // Command: NavigationCommands.LastPage CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), NavigationCommands.LastPage, executedHandler, canExecuteHandler); // no key gesture // Command: NavigationCommands.GoToPage CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), NavigationCommands.GoToPage, executedHandler, canExecuteHandler); // no key gesture // Command: ApplicationCommands.Print CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), ApplicationCommands.Print, executedHandler, canExecuteHandler, new KeyGesture(Key.P, ModifierKeys.Control)); // Command: ApplicationCommands.CancelPrint CommandHelpers.RegisterCommandHandler(typeof(DocumentViewerBase), ApplicationCommands.CancelPrint, executedHandler, canExecuteHandler); // no key gesture // Register editing command handlers - After our commands to let editor handle them first TextEditor.RegisterCommandHandlers(typeof(DocumentViewerBase), /*acceptsRichContent:*/true, /*readOnly:*/!IsEditingEnabled, /*registerEventListeners*/true); } /// <summary> /// Central handler for CanExecuteRouted events fired by Commands directed at DocumentViewerBase. /// </summary> /// <param name="target">The target of this Command, expected to be DocumentViewerBase</param> /// <param name="args">The event arguments for this event.</param> private static void CanExecuteRoutedEventHandler(object target, CanExecuteRoutedEventArgs args) { DocumentViewerBase dv = target as DocumentViewerBase; Invariant.Assert(dv != null, "Target of CanExecuteRoutedEventHandler must be DocumentViewerBase."); Invariant.Assert(args != null, "args cannot be null."); // DocumentViewerBase is capable of execution of the majority of its commands. // Special rules: // a) Print command is enabled when Document is attached and printing is not in progress. // b) CancelPrint command is enabled only during printing. if (args.Command == ApplicationCommands.Print) { args.CanExecute = (dv.Document != null) && (dv._documentWriter == null); args.Handled = true; } else if (args.Command == ApplicationCommands.CancelPrint) { args.CanExecute = (dv._documentWriter != null); } else { args.CanExecute = true; } } /// <summary> /// Central handler for all ExecutedRouted events fired by Commands directed at DocumentViewerBase. /// </summary> /// <param name="target">The target of this Command, expected to be DocumentViewerBase.</param> /// <param name="args">The event arguments associated with this event.</param> private static void ExecutedRoutedEventHandler(object target, ExecutedRoutedEventArgs args) { DocumentViewerBase dv = target as DocumentViewerBase; Invariant.Assert(dv != null, "Target of ExecuteEvent must be DocumentViewerBase."); Invariant.Assert(args != null, "args cannot be null."); // Now we execute the method corresponding to the Command that fired this event; // each Command has its own protected virtual method that performs the operation // corresponding to the Command. if (args.Command == NavigationCommands.PreviousPage) { dv.OnPreviousPageCommand(); } else if (args.Command == NavigationCommands.NextPage) { dv.OnNextPageCommand(); } else if (args.Command == NavigationCommands.FirstPage) { dv.OnFirstPageCommand(); } else if (args.Command == NavigationCommands.LastPage) { dv.OnLastPageCommand(); } else if (args.Command == NavigationCommands.GoToPage) { // Ignore GoToPageCommand, if: // a) there is no value for the page number. // b) the value cannot be converted to Int32. if (args.Parameter != null) { int pageNumber = -1; try { pageNumber = Convert.ToInt32(args.Parameter, System.Globalization.CultureInfo.CurrentCulture); } catch (InvalidCastException) { } catch (OverflowException) { } catch (FormatException) { } if (pageNumber >= 0) { dv.OnGoToPageCommand(pageNumber); } } } else if (args.Command == ApplicationCommands.Print) { dv.OnPrintCommand(); } else if (args.Command == ApplicationCommands.CancelPrint) { dv.OnCancelPrintCommand(); } else { Invariant.Assert(false, "Command not handled in ExecutedRoutedEventHandler."); } } #endregion Commands #region Static Methods /// <summary> /// Called from the event handler to make sure the target is visible in the client /// area. May cause navigation to a different page. /// </summary> /// <param name="sender">The instance handling the event.</param> /// <param name="args">RequestBringIntoViewEventArgs indicates the element and region to scroll into view.</param> private static void HandleRequestBringIntoView(object sender, RequestBringIntoViewEventArgs args) { if (sender != null && sender is DocumentViewerBase) { ((DocumentViewerBase)sender).HandleRequestBringIntoView(args); } } /// <summary> /// The Document has changed and needs to be updated. /// </summary> private static void DocumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Invariant.Assert(d != null && d is DocumentViewerBase); ((DocumentViewerBase) d).DocumentChanged((IDocumentPaginatorSource) e.OldValue, (IDocumentPaginatorSource) e.NewValue); // Since Document state is used to determine CanExecute state, we must invalidate that state. CommandManager.InvalidateRequerySuggested(); } #endregion Static Methods #endregion Private Methods //------------------------------------------------------------------- // // Private Properties // //------------------------------------------------------------------- #region Private Properties /// <summary> /// ITextContainer associated with Document. /// </summary> private ITextContainer TextContainer { get { ITextContainer textContainer = null; if (_document != null) { if (_document is IServiceProvider && CheckFlags(Flags.IsSelectionEnabled)) { textContainer = ((IServiceProvider)_document).GetService(typeof(ITextContainer)) as ITextContainer; } } return textContainer; } } #endregion Private Properties //------------------------------------------------------------------- // // Private Fields // //------------------------------------------------------------------- #region Private Fields private ReadOnlyCollection<DocumentPageView> _pageViews; // Collection of DocumentPageViews presenting paginated Document. private FrameworkElement _textEditorRenderScope; // RenderScope associated with the TextEditor. private MultiPageTextView _textView; // ITextView associated with DocumentViewer. private TextEditor _textEditor; // TextEditor associated with DocumentViewer. private IDocumentPaginatorSource _document; // IDocumentPaginatorSource representing Document. private Flags _flags; // Flags reflecting various aspects of object's state. #if !DONOTREFPRINTINGASMMETA private System.Windows.Xps.XpsDocumentWriter _documentWriter; // DocumentWriter used for printing. #endif // DONOTREFPRINTINGASMMETA private static bool IsEditingEnabled = false; // A flag enabling text editing within a document viewer // accessible only through reflection. #endregion Private Fields //------------------------------------------------------------------- // // Private Types // //------------------------------------------------------------------- #region Private Types /// <summary> /// Flags reflecting various aspects of viewer's state. /// </summary> [System.Flags] private enum Flags { // free bit = 0x10, IsSelectionEnabled = 0x20, // Is text selection enabled. DocumentAsLogicalChild = 0x40, // Is Document part of logical tree. } /// <summary> /// State of BringIntoView operation. /// </summary> private class BringIntoViewState { internal BringIntoViewState(DocumentViewerBase source, ContentPosition contentPosition, DependencyObject targetObject, Rect targetRect) { this.Source = source; this.ContentPosition = contentPosition; this.TargetObject = targetObject; this.TargetRect = targetRect; } internal DocumentViewerBase Source; internal ContentPosition ContentPosition; internal DependencyObject TargetObject; internal Rect TargetRect; } #endregion Private Types //------------------------------------------------------------------- // // IAddChild // //------------------------------------------------------------------- #region IAddChild /// <summary> /// Called to add the object as a Child. /// </summary> /// <param name="value">Object to add as a child.</param> /// <remarks>DocumentViewerBase only supports a single child of type IDocumentPaginatorSource.</remarks> void IAddChild.AddChild(Object value) { ArgumentNullException.ThrowIfNull(value); // Check if Content has already been set. if (this.Document != null) { throw new InvalidOperationException(SR.DocumentViewerCanHaveOnlyOneChild); } // Only IDocumentPaginatorSource is a valid content. IDocumentPaginatorSource document = value as IDocumentPaginatorSource; if (document == null) { throw new ArgumentException(SR.DocumentViewerChildMustImplementIDocumentPaginatorSource, nameof(value)); } this.Document = document; } /// <summary> /// Called when text appears under the tag in markup /// </summary> /// <param name="text">Text to add to the Object.</param> /// <remarks>DocumentViewer does not support Text children.</remarks> void IAddChild.AddText(string text) { XamlSerializerUtil.ThrowIfNonWhiteSpaceInAddText(text, this); } #endregion IAddChild //------------------------------------------------------------------- // // IServiceProvider // //------------------------------------------------------------------- #region IServiceProvider /// <summary> /// Returns service objects associated with this control. /// </summary> /// <param name="serviceType">Specifies the type of service object to get.</param> object IServiceProvider.GetService(Type serviceType) { object service = null; ArgumentNullException.ThrowIfNull(serviceType); // Following services are available: // (1) TextView - wrapper for TextViews exposed by PageViews. // (2) TextContainer - the service object is retrieved from Document. if (serviceType == typeof(ITextView)) { service = _textView; } else if (serviceType == typeof(TextContainer) || serviceType == typeof(ITextContainer)) { service = this.TextContainer; } return service; } #endregion IServiceProvider } }