none
Problem mit dem Speichern von Dokumenten via Office Interop

    Frage

  • Hallo,

    ich habe eine Anwendung, welche bis zu 200 mal am Tag Word via Interop mit einem neuen Dokument startet. Es geht hierbei um das Schreiben von Briefen, wobei Adresse, etc. automatisch von der Anwendung eingefügt werden.

    Ich starte Word wie folgt:

    protected static Application GetWordApplication()
    {
        try
        {
            if (_application == null)
            {
                // Check whether there is an Outlook process running.
                if (Process.GetProcessesByName("WINWORD").Count() > 0)
                {
                    _application = Marshal.GetActiveObject("Word.Application") as Application;
                }
                else
                {
                    _application = new Application();
                }
            }
            return _application;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Konnte Word nicht starten!", ex);
        }
    }

    Um auf das Schließen des Dokuments seitens des Anwenders reagieren zu können, wird ein Listener registriert: 

    doc.Application.DocumentBeforeClose += Application_DocumentBeforeClose;

    Beim Schließen des Dokuments wird dann das Dokument automatisch gespeichert. Damit wird verhindert, dass eine Nachfrage von Word kommt, ob die Änderungen gespeichert werden sollen.

    private void Application_DocumentBeforeClose(Document Doc, ref bool Cancel)
    {
        if (!_closed) // Sicherstellen, dass die Routine nur einmal durchlaufen wird.
        {
            _closed = true;
            try
            {
                int docId = Doc.DocID;
                if (docId == _docId)
                {
                    _doc.Application.DocumentBeforeClose -= Application_DocumentBeforeClose;
                    _doc.SaveAs(_file);
                }
            }
            catch (Exception ex)
            {
                // ...
            }
            finally
            {
                _doc = null;
            }
        }
    }

    Es kommt hier immer wieder zu Problemen. Zum Beispiel kommt sowohl beim Start von Word als auch beim Schließen oft der Fehler "Der RPC-Server ist nicht verfügbar". Manchmal ist es auch so, dass Word in einen "inkonsistenten Zustand" verfällt und beim Öffnen egal welchen Dokuments nur noch eine leere Seite anzeigt. Hier hilft dann nur noch das Beenden des WINWORD-Prozesses.

    Wie gehe ich mit den hier beschriebenen Fehlern um? Diese treten nach keinem bestimmten Muster auf. Manchmal geht es, manchmal nicht.

    Was besagt die Meldung  "Der RPC-Server ist nicht verfügbar" und worin liegt die Ursache? Und wie kann es sein, dass Word in einen Zustand verfällt, wo nur noch - egal bei welchem Dokument - leere Seiten angezeigt werden?

    Gruß

    Donnerstag, 11. Oktober 2018 09:02

Antworten

  • Hi Abid,
    es sieht so aus, als würde der neue Start in den Zeitraum fallen, wo die vorherige Nutzung noch nicht abgeschlossen ist. Ich würde auf die Nutzung einer vorhandenen Instanz verzichten und jedes Mal eine neue Instanz starten. In dieser neuen Instanz würde ich erst einmal alle programmatischen Aktivitäten (Übertragung von Daten in das Dokument) unsichtbar und nur mit Range-Objekten ausführen. Danach würde ich die Instanz sichtbar schalten und keine weiteren Programmaktivitäten außer BeforeClose ausführen. Ich habe die Erfahrung gemacht, dass die parallele Arbeit des Programmes und der Oberfläche Probleme bereitet, insbesondere, wenn man Select und Activate im Programm nutzt.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 11. Oktober 2018 10:36

Alle Antworten

  • Hi Abid,
    es sieht so aus, als würde der neue Start in den Zeitraum fallen, wo die vorherige Nutzung noch nicht abgeschlossen ist. Ich würde auf die Nutzung einer vorhandenen Instanz verzichten und jedes Mal eine neue Instanz starten. In dieser neuen Instanz würde ich erst einmal alle programmatischen Aktivitäten (Übertragung von Daten in das Dokument) unsichtbar und nur mit Range-Objekten ausführen. Danach würde ich die Instanz sichtbar schalten und keine weiteren Programmaktivitäten außer BeforeClose ausführen. Ich habe die Erfahrung gemacht, dass die parallele Arbeit des Programmes und der Oberfläche Probleme bereitet, insbesondere, wenn man Select und Activate im Programm nutzt.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 11. Oktober 2018 10:36
  • Habe das mal eingebaut, dass jedes mal eine neue Instanz vom Application-Objekt erstellt wird.

    Jetzt habe ich aber das Problem, dass der Handler Application_DocumentBeforeClose nicht mehr aufgerufen wird ... genauer: er wird nur dann aufgerufen, wenn ich einen Breakpoint am Beginn des Handler-Codes setze ...

    Nachtrag: außerdem ist es natürlich recht Ressourcen-intensiv, 200 mal am Tag den Word-Prozess zu starten und ihn dann wieder zu schließen. Hat auch den Nachteil, dass es jedes mal etwas dauert, bis Word am Bildschirm erscheint.

    Das führt mich zu der Frage, ob Word Interop grundsätzlich für dieses Anwendungs-Szenario geeignet ist. Kann mir da jemand was zu sagen?

    Donnerstag, 11. Oktober 2018 15:37
  • Hi Abid,
    ich habe das mal mit meinem Laptop mit Windows 10 und Office 2016 getestet. Der erstmalige Start von Word dauert bei mir höchstens 25 Sekunden. Ein nächster Start von Word kurze Zeit später dauert im Mittel lediglich 4 Sekunden. Bei 200 Mal pro Tag sind solche Zeiten problemlos verkraftbar. Auch kommt das DocumentBeforeClose jedes Mal stabil. Getestet habe ich das mit dem Start mehrerer Instanzen parallel.

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Donnerstag, 11. Oktober 2018 19:07
  • Erstmal danke für die schnelle Antwort.

    Bei mir wird das Dokument aus einer WPF-Applikation heraus geöffnet und hier scheint das DocumentBeforeClose irgendwie nicht zuverlässig zu funktionieren.

    Wenn ich dasselbe in einer Konsolen-Applikation mache wird das DocumentBeforeClose zuverlässig ausgeführt.

    Blöd ist, dass ich keine Idee habe, woran das liegen könnte.

    Letztlich geht es mir einfach darum, dass der Speichern-Dialog ("Möchten Sie Ihre Änderungen an Dokument1 speichern?") nicht angezeigt werden soll, sondern mit dem Schließen des Dokuments soll immer gespeichert werden.


    Donnerstag, 11. Oktober 2018 19:31
  • Hi Abid,
    ich vermute, dass in Deinem Code ein Fehler ist. Vermutlich werden bei Dir threadübergreifende Zugriffe nicht richtig behandelt. Zeig mal, wie Du das asynchrone BeforeClose-Ereignis in den UI-Thread umleitest?

    Hier mal eine WPF Demo auf Basis des MVVM Entwurfsmusters, die problemlos funktioniert. Im WPF-Window wird mit einem Button eine neue Word-Instanz gestartet. In der darunter liegenden ListBox werden die Ereignisse protokolliert, um vor allem die Zeit zu ermittelt. Wichtig ist zu beruecksichtigen, dass beim Test mit leeren Dokumenten kein BeforeClose ausgeloest wird.

    <Window x:Class="WpfApp1.Window74"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApp1"
            mc:Ignorable="d"
            Title="Window74" Height="450" Width="800">
      <Window.Resources>
        <local:Window74VM x:Key="vm"/>
      </Window.Resources>
      <Grid DataContext="{StaticResource vm}">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Start Word" Command="{Binding Cmd}" />
        <ListBox Grid.Row="1" ItemsSource="{Binding Protokoll}"/>
      </Grid>
    </Window>

    Dazu der Code:

    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Input;
    using Word = Microsoft.Office.Interop.Word;
    
    namespace WpfApp1
    {
      internal class Window74VM : INotifyPropertyChanged
      {
        private CollectionViewSource cvs = new CollectionViewSource();
        private ObservableCollection<string> col = new ObservableCollection<string>();
        private int index = 0;
    
        /// <summary>
        /// Eigenschaft für die Anzeige eines Protokolls
        /// </summary>
        public ICollectionView Protokoll
        { get { if (cvs.Source == null) cvs.Source = col; return cvs.View; } }
    
        public ICommand Cmd { get { return new RelayCommand(CmdExec); } }
    
        /// <summary>
        /// Ereignismethode, die beim Klick auf den Button ausgeführt wird
        /// </summary>
        /// <param name="obj">CommandParameter</param>
        private void CmdExec(object obj)
        {
          index++;
          DemoWord demo = new DemoWord();
          demo.Meldung += (sender, e) => { col.Insert(0, e.Message); };
          demo.Execute(index);
        }
    
        #region  OnPropertyChanged - an Oberfläche Änderungen der Eigenschaftswerte melden
        public event PropertyChangedEventHandler PropertyChanged;
        internal void OnPropertyChanged([CallerMemberName] string propName = "") =>
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
        #endregion
    
        /// <summary>
        /// Kapselung des Zugriffes auf Word
        /// </summary>
        internal class DemoWord
        {
          SynchronizationContext sc;
          /// <summary>
          /// Im Construktor den aktuellen SynchronizationContext (UI-Thread) merken
          /// </summary>
          internal DemoWord() => sc = SynchronizationContext.Current;
    
          /// <summary>
          /// Ereignis, um der UI etwas zu melden
          /// </summary>
          internal event EventHandler<MeldungEventArgs> Meldung;
    
          // Laufender Aufruf einer Word-Instanz 
          private int i = 0;
    
          /// <summary>
          /// Methode zum Start von Word
          /// </summary>
          /// <param name="index"></param>
          internal void Execute(int index)
          {
            i = index;
            Meldung?.Invoke(this, new MeldungEventArgs() { Message = $"Start {i}: {DateTime.Now.ToLongTimeString()}" });
            Word.Application wdApp = new Word.Application();
            wdApp.DocumentBeforeClose += Application_DocumentBeforeClose;
            wdApp.Documents.Add();
            wdApp.Visible = true;
            Meldung?.Invoke(this, new MeldungEventArgs() { Message = $"Dok {index} geöffnet: {DateTime.Now.ToLongTimeString()}" });
          }
    
          /// <summary>
          /// Ereignis beim Schließen von Word
          /// </summary>
          /// <param name="Doc"></param>
          /// <param name="Cancel"></param>
          private void Application_DocumentBeforeClose(Word.Document Doc, ref bool Cancel)
          {
            sc.Post(new SendOrPostCallback(OnMeldung), $"Word {i} wird geschlossen.");
          }
    
          /// <summary>
          /// Methode zum Melden einer Information an den UI-Thread
          /// </summary>
          /// <param name="msg"></param>
          private void OnMeldung(object msg) => Meldung?.Invoke(this, new MeldungEventArgs() { Message = msg.ToString() });
        }
    
        /// <summary>
        /// Ereignisargumente für das Weiterleiten einer Nachricht
        /// </summary>
        internal class MeldungEventArgs { internal string Message { get; set; } }
      }
    }


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks


    Freitag, 12. Oktober 2018 06:08
  • Hallo Peter,

    ich mache das z.Zt. so (die Methode Application_DocumentBeforeClose wird nicht aufgerufen):

    public class DokumentEditViewModelBase : EditViewModelBase
    {
        public ICommand EditWord { get; set; }
    
        // Word-Vorlage
        private string _vorlage;
        
        // Pfad zur temporären Word-Datei
        private string _tempFile;
    
        public DokumentEditViewModelBase(IBriefModel model, Data.Einstellung vorlage, Data.Person betrifft) : base(model)
        {   
            EditWord =  new RelayCommand(EditWordExecute);
            // Initialisierung ...
        }
    
        protected void EditWordExecute(object obj)
        {
            // Mittels OpenXML eine Word-Datei erstellen und in temporärer Datei speichern
            _tempFile = WordService.CreateBrief(_vorlage);
            
            // Word-Dokument öffnen
            var application = new Microsoft.Office.Interop.Word.Application();
            application.Visible = true;
            application.DocumentBeforeClose += Application_DocumentBeforeClose;
    
            Document = application.Documents.Open(_tempFile);
            application.ActiveDocument.Saved = false;
        }
    
        private void Application_DocumentBeforeClose(Microsoft.Office.Interop.Word.Document Doc, ref bool Cancel)
        {
            Log.Debug("Application_DocumentBeforeClose ...");
            
            // Dokument speichern
            Document.Save();
    
            // Jetzt alles in der Datenbank speichern
            Log.Debug("Änderungen in DB speichern ...");
            SaveExecute();
        }
        
        private void SaveExecute()
        {
            // Resultat der Bearbeitung in DB speichern
        }
    }

    Verwende ich wie von Dir beschrieben einen SynchronizationContext (s.u.), wird ebenfalls Application_DocumentBeforeClose nur manchmal ausgeführt. Wenn es ausgeführt wird, erhalte ich beim Aufruf von Document.Save() erhalte ich folgenden Fehler: System.Runtime.InteropServices.COMException (0x80010001): Aufruf wurde durch Aufgerufenen abgelehnt. (Ausnahme von HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))

    public  class DokumentEditViewModelBase
    {
        public ICommand EditWord { get; set; }
    
        // Word-Vorlage
        private string _vorlage;
        
        // Pfad zur temporären Word-Datei
        private string _tempFile;
    
        private SynchronizationContext _sc;
    
        public DokumentEditViewModelBase(IBriefModel model, Data.Einstellung vorlage, Data.Person betrifft) : base(model)
        {   
            _sc = SynchronizationContext.Current;
            EditWord =  new RelayCommand(EditWordExecute);
            // Initialisierung ...
        }
    
        protected void EditWordExecute(object obj)
        {
            // Mittels OpenXML eine Word-Datei erstellen und in temporärer Datei speichern
            _tempFile = WordService.CreateBrief(_vorlage);
            
            // Word-Dokument öffnen
            var application = new Microsoft.Office.Interop.Word.Application();
            application.Visible = true;
            application.DocumentBeforeClose += Application_DocumentBeforeClose;
    
            Document = application.Documents.Open(_tempFile);
            application.ActiveDocument.Saved = false;
        }
    
        private void Application_DocumentBeforeClose(Microsoft.Office.Interop.Word.Document Doc, ref bool Cancel)
        {
            _sc.Post(new SendOrPostCallback(OnWordClose), "Word wird geschlossen.");
        }
        
        private void OnWordClose(object msg)
        {
            Log.Debug("Application_DocumentBeforeClose ...");
            
            // Dokument speichern, Fehler: "Aufruf wurde durch Aufgerufenen abgelehnt".
            Document.Save();
    
            // Jetzt alles in der Datenbank speichern
            Log.Debug("Änderungen in DB speichern ...");
            SaveExecute();
        }
        
        private void SaveExecute()
        {
            // Resultat der Bearbeitung in DB speichern
        }
    }





    Samstag, 13. Oktober 2018 09:11
  • Hi Abid,
    Du lässt zuerst die Bearbeitung durch den Anwender zu, bevor Du Dich an das Ereignis hängst. Hast Du mal geprüft, ob die Ereignisroutine überhaupt angehängt wurde, bevor das Ereignis ausgelöst wurde?

    Auf welches Dokument wirkt ActiveDocument.Saved? Wie stellst Du sicher, dass bei mehreren geöffneten Dokumenten auch das wirklich gewünschte betroffen ist?

    Den SynchronizationContext und Post benötigst Du nur für die Programmstücke, die im UI-Thread auszuführen sind. Für Document.Save kann ich mir nicht vorstellen, dass das in einem anderen Thread auszuführen ist.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks


    Samstag, 13. Oktober 2018 17:46
  • Nochmals danke für die ausführliche Antwort. Ich hab's mit einem anderen Event-Handler hinbekommen. 

    Ich verwende nun den Close-Handler von Microsoft.Office.Interop.Word.DocumentEvents_Event. Das funktioniert nun endlich :-)

    Das Ganze hat auch den Vorteil, dass der Handler - anders als bei Application_DocumentBeforeClose - für ein bestimmtes Dokument ausgeführt wird und nicht für alle geöffneten Dokumente.

    Warum es mit Application_DocumentBeforeClose nicht ging, ist mir nach wie vor nicht klar. Bei Deinem DemoWord-Beispiel hat es ja wunderbar funktioniert.

    public class DokumentEditViewModelBase
    {
        // ...
    
        protected void EditWordExecute(object obj)
        {
            // Word-Dokument erstellen
            _tempFile = WordService.CreateBrief(vorlage.DataObject.Wert, DataObject);
            // Word-Dokument öffnen
            Document = WordService.Open(_tempFile, OnClose);
        }
    
        private void OnClose()
        {
            // Jetzt alles in der Datenbank speichern
            SaveExecute(_currentView);
        }
    }
    
    public class WordService
    {
        public static Document Open(string file, Action onClose, bool saveOnClose = true, bool quitAfterClose = true)
        {
            var app = CreateWordApplication();
    
            object missing = System.Reflection.Missing.Value;
            object visible = false;
            object fileName = file;
    
            Document doc = app.Documents.Open(
                ref fileName, 
                ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, 
                ref visible, 
                ref missing, ref missing, ref missing, ref missing);
            DocumentEvents_Event docEvents = (Microsoft.Office.Interop.Word.DocumentEvents_Event) doc;
            docEvents.Close += () =>
            {
                if (saveOnClose)
                    doc.Save();
    
                if (quitAfterClose)
                    app.Quit();
    
                onClose();
            };
    
            app.Visible = true;
    
            doc.Activate();
            app.ActiveWindow.ActivePane.View.Type = WdViewType.wdPrintView;
            app.ActiveWindow.ScrollIntoView(doc.Range(), true);
    
            return doc;
        }
    }

    Sonntag, 14. Oktober 2018 16:58