Office File API & Office-Inspired Desktop UI Controls — Tips and Tricks (October 2022)

Office-Inspired Products
12 October 2022

This post includes a series of interesting support tickets answered throughout August and September along with enhancements made to our Office-inspired product line. We hope you find the contents of this post interesting and of business value. As always, should you have any questions about DevExpress Office products, feel free to submit a support ticket via the DevExpress Support Center.

Word Processing

Tips and Tricks – Fields

Document fields are an integral part of our Word Processing API. This section includes a few document field-related support tickets:

  1. How to highlight fields in a different color within a selection

    The FieldOptions.HighlightMode and FieldOptions.HighlightColor properties allow you to highlight fields and specify highlight color. All fields are highlighted all the time, whether they are selected or not. But you can highlight them with a different color when selecting a text block.

    Our RichEditControl includes a SelectionChanged event. This event allows you to determine whether a given selection contains fields. Handle the BeforePagePaint event to change the field’s highlight color as needs dictate.

    In the SelectionChanged event handler, retrieve the selected paragraph based on caret position. Determine whether the paragraph contains fields and use the RichEditControl.DocumentLayout.GetElement method to obtain all FieldHighlightAreaBox elements related to these fields. Make sure that you set the FieldOptions.HighlightMode property to Always. In this instance, the FieldHighlightAreaBox object is detected for each field.

    private List<FieldHighlightAreaBox> highlightBoxes;
    private void richEditControl1_SelectionChanged(object sender, EventArgs e) {
      highlightBoxes = new List<FieldHighlightAreaBox>();
      DocumentPosition caret = richEditControl1.Document.CaretPosition;
      SubDocument subDoc = caret.BeginUpdateDocument();
    
      DocumentRange selection = richEditControl1.Document.Selection;
      int selectionStart = selection.Start.ToInt();
      int selectionEnd = selection.End.ToInt();
      var fields = subDoc.Fields.Where(field => field.Range.End.ToInt() 
      	>= selectionStart && field.Range.Start.ToInt() <= selectionEnd).ToList();
      foreach (Field field in fields) {
          DocumentPosition start =
          	field.ShowCodes ? field.CodeRange.Start : field.ResultRange.Start;
          FieldHighlightAreaBox fhBox =
          	richEditControl1.DocumentLayout.GetElement(start, LayoutType.FieldHighlightAreaBox)
            as FieldHighlightAreaBox;
          if (fhBox != null) {
              highlightBoxes.Add(fhBox);
              start =
              	subDoc.CreatePosition(fhBox.Range.Start + fhBox.Range.Length);
          }
        }
        caret.EndUpdateDocument(subDoc);
        this.BeginInvoke(new Action(() =>
        {
           richEditControl1.Refresh();
        }));
    }

    Implement a custom PagePainter class and assign the class instance to the Painter property in the RichEditControl.BeforePagePaint event handler. Override the DrawFieldHighlightAreaBox method of the custom painter and check whether the list of FieldHighlightAreaBox elements contains the element passed to the method as a parameter. If it is, invoke the base.DrawFieldHighlightAreaBox method.

    public class CustomPagePainter : PagePainter {
      private List<fieldhighlightareabox> highlightBoxes;
      public CustomPagePainter(List<fieldhighlightareabox> highlightBoxes) {
          this.highlightBoxes = highlightBoxes;
      }
      public override void DrawFieldHighlightAreaBox(FieldHighlightAreaBox fieldHighlightAreaBox) {
          if (highlightBoxes.Contains(fieldHighlightAreaBox))
             base.DrawFieldHighlightAreaBox(fieldHighlightAreaBox);
       }
    }
    
  2. How to determine whether MERGEFIELD field does not have a value

    You can iterate through the Document.Fields collection and call the Document.GetText method (pass Field.ResultRange to the method as a parameter) to get the field value. If the GetText method returns an empty string, you may highlight the field or insert static content in the field result area to indicate that the field does not have a valid value.

    private void HighlightEmptyFields() {
         foreach (Field field in richEditControl1.Document.Fields) {
             string strResult = richEditControl1.Document.GetText(field.ResultRange);
             string strCode = richEditControl1.Document.GetText(field.CodeRange);
    
             if (strResult == "") {
                richEditControl1.Document.InsertText(field.ResultRange.Start, "Empty Value!");
                CharacterProperties rangeProperties =
                	richEditControl1.Document.BeginUpdateCharacters(field.ResultRange);
                rangeProperties.BackColor = Color.DarkBlue;
                richEditControl1.Document.EndUpdateCharacters(rangeProperties);
             }
         }
    }
    
  3. How to determine field type (T1114958)

    Retrieve the field value as described above. Parse the retrieved field value to determine field type:

    foreach (Field field in richEditControl1.Document.Fields) {
        string strResult =
        	richEditControl1.Document.GetText(field.ResultRange);
        string strCode =
        	richEditControl1.Document.GetText(field.CodeRange);
        string[] subs = strCode.TrimStart().Split(' ');
    
        if (subs[0] == "DATE") {
            // your code here
        }
    }
    
  4. How to automatically insert a space after an inserted field (T1115159)

    You can implement a custom InsertMergeFieldCommand (for merge fields) or InsertFieldCommand (for regular fields) and override its Execute method. In this method, you can call the SubDocument.InsertText method after the command is executed to insert the space character programmatically.

    public class CustomInsertMergeFieldCommand : InsertMergeFieldCommand {
       //…
       public override void Execute() {
          base.Execute();
          SubDocument subdoc =
          	this.Control.Document.CaretPosition.BeginUpdateDocument();
          subdoc.InsertText(this.Control.Document.CaretPosition, " ");
          this.Control.Document.CaretPosition.EndUpdateDocument(subdoc);
       }
    }
    
    var myCommandFactory =
    	new CustomRichEditCommandFactoryService(richEditControl,
        richEditControl.GetService<IRichEditCommandFactoryService>());
    richEditControl.ReplaceService<IRichEditCommandFactoryService>(myCommandFactory);
    
    public class CustomRichEditCommandFactoryService : IRichEditCommandFactoryService {
        readonly IRichEditCommandFactoryService service;
        readonly RichEditControl control;
    
        public CustomRichEditCommandFactoryService(RichEditControl control, IRichEditCommandFactoryService service) {
            DevExpress.Utils.Guard.ArgumentNotNull(control, "control");
            DevExpress.Utils.Guard.ArgumentNotNull(service, "service");
            this.control = control;
            this.service = service;
        }
    
        public RichEditCommand CreateCommand(RichEditCommandId id) {
            if(id == RichEditCommandId.InsertMailMergeField) {
               return new CustomInsertMergeFieldCommand (control);
            }        
            return service.CreateCommand(id);
        }
    }
  5. How to delete a field when deleting its brackets

    Like the example above, you can overwrite built-in commands (DeleteCommand and BackSpaceKeyCommand to be exact) to complete the task. The command should remove the corresponding field when the caret is positioned at the start or the end of the field.

    public class CustomBackSpaceKeyCommand : BackSpaceKeyCommand  {
                public CustomBackSpaceKeyCommand(IRichEditControl control) : base(control)  { }
                // use the same code in the DeleteCommand override 
                public override void Execute() {
                    bool shouldExecute = true;
                    DocumentPosition caret = Control.Document.CaretPosition;
                    SubDocument doc = caret.BeginUpdateDocument();
                    foreach (var field in doc.Fields) {
                        if (caret.ToInt() == field.Range.End.ToInt()) {
                            doc.Delete(field.Range);
                            shouldExecute = false;
                            break;
                        }
                    }
                    caret.EndUpdateDocument(doc);
                    if (shouldExecute)
                        base.Execute();
                }
    }
    
    var myCommandFactory =
    	new CustomRichEditCommandFactoryService(richEditControl,
        richEditControl.GetService<IRichEditCommandFactoryService>());
    richEditControl.ReplaceService<IRichEditCommandFactoryService>(myCommandFactory);
    
    public class CustomRichEditCommandFactoryService : IRichEditCommandFactoryService {
        readonly IRichEditCommandFactoryService service;
        readonly RichEditControl control;
    
        public CustomRichEditCommandFactoryService(RichEditControl control, IRichEditCommandFactoryService service) {
            DevExpress.Utils.Guard.ArgumentNotNull(control, "control");
            DevExpress.Utils.Guard.ArgumentNotNull(service, "service");
            this.control = control;
            this.service = service;
        }
    
        public RichEditCommand CreateCommand(RichEditCommandId id) {
            if(id == RichEditCommandId.BackSpaceKey) {
                return new CustomInsertMergeFieldCommand (control);
            }        
            return service.CreateCommand(id);
        }
    }
  6. How to preserve images from the INCLUDEPICTURE fields (T1105370)

    You can remove the "\d" switch from the INCLUDEPICTURE field code (you can do this in the UI or in code). Without the switch, the document stores both the link and the picture. Alternatively, you can iterate through the Document.Fields collection, find your INCLUDEPICTURE fields, remove them and insert their pictures instead. To replace fields with their values, you can use the Field.Unlink, FieldCollection.Unlink, or Document.UnlinkAllFields methods.

Spreadsheet Controls and the Excel Export library

Tips & Tricks

  1. How to prevent users from creating a named range from the Name Box
    https://supportcenter.devexpress.com/ticket/details/t1111051

    When a user enters a name in the Spreadsheet Name Box control, our SpreadsheetControl automatically creates a named range for the selection. To avoid this behavior, handle the SpreadsheetNameBoxControl.KeyDown event to catch when the Enter key is pressed. Use the IRangeProvider.Parse method to parse the editor's value. If this method throws InvalidOperationException, restore the SpreadsheetNameBoxControl value based on the current selection.

    private void spreadsheetNameBoxControl_KeyDown(object sender, KeyEventArgs e) { 
        if(e.KeyCode == Keys.Enter) {
             string value = spreadsheetNameBoxControl.EditValue.ToString(); 
             try {
                  CellRange range =
                  	spreadsheetControl.Document.Range.Parse(value); 
              } 
              catch(InvalidOperationException) { 
                   spreadsheetNameBoxControl.EditValue =
                   	spreadsheetControl.Selection.GetReferenceA1(); 
              } 
        } 
    }
  2. How to track when a user changes the selected item in an in-place ComboBox editor
    https://supportcenter.devexpress.com/ticket/details/t1112346

    You can handle the CustomCellEdit event to manually create the required instance of a custom editor and handle its events (for example, SelectedValueChanged for RepositoryItemComboBox).

    using DevExpress.XtraSpreadsheet;
    
    private void SpreadsheetControl_CustomCellEdit(object sender, SpreadsheetCustomCellEditEventArgs e) { 
      RepositoryItemComboBox ric = new RepositoryItemComboBox();
      ric.SelectedValueChanged += Ric_SelectedValueChanged;
      ric.Items.AddRange(new string[] { "test1", "test2", "test3" });
      e.RepositoryItem = ric; 
    } 
    
    private void Ric_SelectedValueChanged(object sender, EventArgs e) { 
      ComboBoxEdit cbe = sender as ComboBoxEdit; 
      //your code.. 
    }
    
  3. Excel Export - How to correctly generate cells with percentage values
    https://supportcenter.devexpress.com/ticket/details/t1115225

    When you generate a cell with a numeric value with percentage formatting and preview the document, you may notice that Excel and our Spreadsheet component multiply the value by 100. To avoid this behavior and display the original number with the percentage symbol, divide the cell value by 100.

    cell.Value = Convert.ToDouble("94.5") / 100;
    cell.ApplyFormatting(XlNumberFormat.Percentage2);

PDF Document API

Tips & Tricks

  1. How to save handwriting from a PDF file to an image
    https://supportcenter.devexpress.com/ticket/details/T1110307

  2. How to export a PDF page to a byte array
    https://supportcenter.devexpress.com/ticket/details/T1109967

    To save a document page into a byte array, extract the required page to a new PdfDocumentProcessor instance. Once complete, call the PdfDocumentProcessor.SaveDocument method to save the document with the extracted page to a MemoryStream instance and obtain stream bytes.

    using DevExpress.Pdf;
    
    using (var pdfDocumentProcessor = new PdfDocumentProcessor()) {
     pdfDocumentProcessor.LoadDocument(@"Documents\Example.pdf");
     
     using (PdfDocumentProcessor target = new PdfDocumentProcessor()) {
           target.CreateEmptyDocument("..\\..\\ExtractedFirstPage.pdf");
           target.Document.Pages.Add(pdfDocumentProcessor.Document.Pages[0]);
            using (var stream = new MemoryStream())  {
                target.SaveDocument(stream);
                var bytes = stream.ToArray();
            }
        }
    }
  3. How to autofit text in the text box form field
    https://supportcenter.devexpress.com/ticket/details/T1114796

Enhancements

  1. You can now specify rotation angle for rubber stamp and free text annotations
    https://supportcenter.devexpress.com/ticket/details/t1066431/

    New PdfRubberStampAnnotationFacade.RotationAngle and PdfFreeTextAnnotationFacade.RotationAngle options allow you to set rotation angle for a rubber stamp or free text annotations. We also added the PdfPageFacade.RotationAngle property to retrieve a page’s rotation angle. If you insert an annotation to a rotated page, make certain that the annotation’s RotationAngle property value equals the PdfPageFacade.RotationAngle property.

    PdfDocumentProcessor processor = new PdfDocumentProcessor();
    processor.LoadDocument("rotated_doc.pdf");
    PdfPageFacade pageFacade = processor.DocumentFacade.Pages[0];
    PdfRectangle rubberStampRectangle = new PdfRectangle(300, 300, 450, 450);
    PdfRubberStampAnnotationFacade rubberStamp = pageFacade.AddRubberStampAnnotation(rubberStampRectangle, "stamp.pdf", 1);
    rubberStamp.RotationAngle = pageFacade.RotationAngle;
    
  2. A new PdfDocumentProcessor.CreateBitmap overload allows you to create a bitmap with a specific DPI value
    https://supportcenter.devexpress.com/ticket/details/T1114895

    The method’s PdfPageRenderingParameters argument allows you to specify the target DPI or the length of the largest image edge.

    using (var pdfDocumentProcessor = new PdfDocumentProcessor()) {
        pdfDocumentProcessor.LoadDocument(@"Documents\Example.pdf");
        PdfPageRenderingParameters parameters =
        	PdfPageRenderingParameters.CreateWithResolution(300);
        pdfDocumentProcessor.CreateBitmap(1, parameters).Save("..\\..\\MyBitmap.bmp");
    }
    

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.