Robuust NetOffice.PowerPointApi gebruik

Robuust een slide kopiëren met NetOffice.PowerPointApi

Situatie

Stel je wilt via programma code in c# een slide kopiëren naar het einde van een andere presentatie. Hoe doe je dat?

Volgens de NetOffice API is dat met het volgende stukje code:

    slide.CopyToClipboard();
    naarPresentatie.Slides.Paste();

Dit is precies hetzelfde als de Microsoft.Office.Interop.Powerpoint API doet. Immers, NetOffice is gemaakt om Microsoft.Office.Interop rechtstreeks te kunnen vervangen zonder code aanpassingen.

De situatie die aandacht verdient is de ‘CopyToClipboard()’ en ‘Paste()’ code. Dit is heel erg ‘vies’: er wordt informatie naar het klembord gekopieerd. (Dit is hetzelfde klembord waar ook je ctrl-c en ctrl-v acties uit een willekeurig Windows programma mee werken.)

For the English version of this article, continue reading my other blogpost.

Stabiliteit probleem

Office Interop, de techniek onder NetOffice en Microsoft.Office, maakt gebruik van een losse koppeling tussen programma’s. Daardoor kan er wel informatie uitgewisseld worden maar dan zonder inbreuk te maken op de geheugenschrijfrechten van betreffende programma’s. Je hebt echter wel last van timing en concurrency problemen.

Het Windows klembord ligt ook buiten je programma. Ook met de toegang tot je klembord heb je last van timing en concurrency problemen.

Gecombineerd levert het kopiëren van slides grote instabiliteit op in je programma.
Ik kreeg bijvoorbeeld deze fouten:

  • System.Runtime.InteropServices.COMException (0x80010105): De server heeft een uitzondering geretourneerd. (Uitzondering van HRESULT: 0x80010105 (RPC_E_SERVERFAULT))
  • System.Runtime.InteropServices.COMException (0x80010001): Aanroep geweigerd door aangeroepene. (Uitzondering van HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))

Oplossing

De oplossing kwam voor mij in drie delen:

  1. Gebruik een stabiele Office Interop client
  2. Wacht op het klembord
  3. Vang interop fouten af en probeer het opnieuw

1 Stabiele Office Interop client

Zoals dit artikel al aangaf ben ik overgegaan op de NetOffice office automation oplossing. Je hebt dan dezelfde aanroep constructies als met Office Interop van Microsoft maar deze is iets stabieler.

Dit pakket kan je binnenhalen met NuGet als je zoekt op NetOffice.Powerpoint.
De officiële website voor NetOffice is https://netoffice.codeplex.com/

(ps. Op de pc waarop je het programma uitvoert heb je nog wel Microsoft Office nodig. NetOffice Interop is alleen een verbinding tussen jouw programma en Office.)

2 Wacht op het klembord

Als je met Office Interop werkt gaat het kopiëren en plakken via het klembord. Je roept echter COM aanroepen aan om het kopiëren en plakken daadwerkelijk uit te voeren.

Om te voorkomen dat je constant COM fouten krijgt doordat de informatie nog niet op het klembord beschikbaar is gebruik ik de volgende code.

using NetOffice.PowerPointApi;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace mppt.Connect
{
    class ClipboardDemo
    {
        private _Presentation _presentatie;

        /// <summary>
        /// Voeg een slide in in de hoofdpresentatie op de volgende positie (hoofdpresentatie werd aangemaakt bij het maken van deze klasse)
        /// </summary>
        /// <param name="slides">de slide die ingevoegd moet worden (voorwaarde is hierbij dat de presentatie waarvan de slide onderdeel is nog wel geopend is)</param>
        public void SlidesKopieNaarPresentatie(IEnumerable<IMppSlide> slides)
        {
            foreach (var slide in slides.ToList())
            {
                slide.CopyToClipboard();
                while (!HasClipboardPowerpointSlideContent())
                {
                    System.Threading.Thread.Sleep(100);
                }
                _presentatie.Slides.Paste();
            }
        }

        private static bool HasClipboardPowerpointSlideContent()
        {
            var data = Clipboard.GetDataObject();
            if (data == null)
                return false;
            var formats = data.GetFormats();
            return formats.Any(f => f.StartsWith("PowerPoint"));
        }
    }
}

Ik zal nu kort per stukje uitleg geven hoe en waarom deze code werkt.

            foreach (var slide in slides.ToList())
            {
                slide.CopyToClipboard();
                while (!HasClipboardPowerpointSlideContent())
                {
                    System.Threading.Thread.Sleep(100);
                }
                _presentatie.Slides.Paste();
            }

Hier worden de slides per stuk gekopieerd.

Na elke kopieer actie laat ik een functie controleren of er iets op het klembord staat wat aan Powerpoint gerelateerd is. Zo lang die informatie er nog niet is wacht ik met Thread.Sleep().

Als die informatie er is kan ik eindelijk een plak actie doen.

        private static bool HasClipboardPowerpointSlideContent()
        {
            var data = Clipboard.GetDataObject();
            if (data == null)
                return false;
            var formats = data.GetFormats();
            return formats.Any(f => f.StartsWith("PowerPoint"));
        }

Dit is de functie die daadwerkelijk controleert of er iets op het klembord staat.

Het klembord zit in System.Windows.Forms. Dit klembord ondersteunt alle mogelijke type data. Ik ben alleen geïnteresseerd of er iets is en of dat van Powerpoint af komt.

De GetFormats() geeft aan hoe je de informatie op het klembord kan interpreteren en waar je het naar toe kan converteren. Met wat trial-and-error kon ik afleiden dat het format met ‘Powerpoint’ moest starten.

Je bent door de klembord controle er nog niet zeker van dat het ook daadwerkelijk een Slide is en niet bijvoorbeeld een Shape. Ook kan het klembord conflicteren met andere kopieer acties die de gebruiker op hetzelfde moment doet. In de praktijk blijkt deze eenvoudige klembord controle voor mij een goede manier om het kopiëren en plakken betrouwbaarder te maken.

3 Vang interop fouten af

-en probeer opnieuw.

De voorgaande klembord code kent nog wat problemen. Allereerst gebeurt het regelmatig dat er zich een COM fout voordoet tijdens het plakken. En daarnaast loop je het risico dat je oneindig wacht op een klembord.

using NetOffice.PowerPointApi;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace mppt.Connect
{
    class CompleteDemo
    {
        private _Presentation _presentatie;

        public int SlidesKopieNaarPresentatie(IEnumerable<IMppSlide> slides, int retryCount = 3)
        {
            var itemsGemist = 0;
            foreach (var slide in slides.ToList())
            {
                slide.CopyToClipboard();
                var gelukt = false;
                for (int currentTry = 1; currentTry < retryCount && gelukt == false; currentTry++)
                {
                    try
                    {
                        gelukt = ExecuteWhen(HasClipboardPowerpointSlideContent, () =>
                        {
                            _presentatie.Slides.Paste();
                        });
                    }
                    catch (System.Runtime.InteropServices.COMException)
                    {

                    }
                }
                if (!gelukt)
                    itemsGemist++;
            }
            return itemsGemist;
        }

        private static bool ExecuteWhen(Func<bool> isTrue, Action action, int minWaitTime = 5, int maxWaitTime = 250, int waitStep = 25, int waitBeforeExecute = 5)
        {
            var waited = 0;
            if (minWaitTime > 0)
                System.Threading.Thread.Sleep(minWaitTime);
            waited += minWaitTime;
            while (waited < maxWaitTime && waitStep > 0 && !isTrue.Invoke())
            {
                System.Threading.Thread.Sleep(waitStep);
                waited += waitStep;
            }
            if (isTrue.Invoke())
            {
                if (waitBeforeExecute > 0)
                    System.Threading.Thread.Sleep(waitBeforeExecute);
                action.Invoke();
                return true;
            }
            return false;
        }

        private static bool HasClipboardPowerpointSlideContent()
        {
            var data = Clipboard.GetDataObject();
            if (data == null)
                return false;
            var formats = data.GetFormats();
            return formats.Any(f => f.StartsWith("PowerPoint"));
        }
    }
}

Ik zal nu kort per stukje uitleg geven hoe en waarom deze code werkt.

Je kunt zien dat er een ‘retry strategie’ rond de combinatie van klembord controle en plakken geplaatst is. Een ‘retry strategie’ is onmisbaar als je met een proces op afstand werkt waar je niet volledige controle over hebt.

                var gelukt = false;
                for (int currentTry = 1; currentTry < retryCount && gelukt == false; currentTry++)

Hier zie je dat ik probeer tot ik een maximaal aantal pogingen gehaald heb of totdat het kopiëren gelukt is.

                        gelukt = ExecuteWhen(HasClipboardPowerpointSlideContent, () =>
                        {
                            _presentatie.Slides.Paste();
                        });

Ik heb voor het niet oneindig controleren van het klembord een algemene retry functie gemaakt. ExecuteWhen() is deze retry functie.

Lees dit stukje code als volgt: Of het gelukt is weet ik uit ExecuteWhen(), ExecuteWhen() zal plakken uitvoeren als HasClipboardPowerpointSlideContent() succesvol is.

        private static bool ExecuteWhen(Func<bool> isTrue, Action action, int minWaitTime = 5, int maxWaitTime = 250, int waitStep = 25, int waitBeforeExecute = 5)
        {
            var waited = 0;
            if (minWaitTime > 0)
                System.Threading.Thread.Sleep(minWaitTime);
            waited += minWaitTime;
            while (waited < maxWaitTime && waitStep > 0 && !isTrue.Invoke())
            {
                System.Threading.Thread.Sleep(waitStep);
                waited += waitStep;
            }
            if (isTrue.Invoke())
            {
                if (waitBeforeExecute > 0)
                    System.Threading.Thread.Sleep(waitBeforeExecute);
                action.Invoke();
                return true;
            }
            return false;
        }

Deze functie kan je voor meer scenario’s gebruiken waar je een ‘retry strategie’ nodig hebt.

            var waited = 0;
            if (minWaitTime > 0)
                System.Threading.Thread.Sleep(minWaitTime);
            waited += minWaitTime;

In deze functie wacht ik in het begin al meteen de minimale wachttijd. Pas na deze minimale wachttijd ga ik controles op de isTrue() functie doen.
Zoals je ziet houd ik de tijd bij die ik in totaal gewacht heb zodat ik na een maximum kan stoppen.

            while (waited < maxWaitTime && waitStep > 0 && !isTrue.Invoke())

Hier is de loop waarmee ik wacht zolang ik nog niet door kan gaan. Maar deze loop zal stoppen als ik een maximale tijd gewacht heb.

            if (isTrue.Invoke())
            {
                if (waitBeforeExecute > 0)
                    System.Threading.Thread.Sleep(waitBeforeExecute);
                action.Invoke();
                return true;
            }

Omdat ik hier kan komen zonder dat ik isTrue() hebt gecontroleerd doe ik hier een controle of ik ook daadwerkelijk door mag gaan.
Ik heb hier nog een extra wachttijd na het detecteren van een goede situatie om met programma’s om te kunnen gaan die te vroeg een gereedmelding vrij geven (wat aannemelijk is bij een klembord).

Pas als dit alles is gedaan word de daadwerkelijke functie uitgevoerd met action() en komt er een melding terug dat ik het uitgevoerd heb.

Afsluitend

Je kunt deze code vinden in mijn liturgie generator project op github. De code staat in dit bestand.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s