Reliable copy a powerpoint slide by code using NetOffice.PowerPointApi
Situation
Imagine you want to copy a slide via program code in c # to the end of another presentation. How do you do that?
According to the NetOffice API this is with the following piece of code:
slide.CopyToClipboard(); naarPresentatie.Slides.Paste();
This is exactly the same as the Microsoft.Office.Interop.Powerpoint API does. After all, NetOffice is made to replace Microsoft.Office.Interop directly without code modifications.
The situation that deserves attention is the ‘CopyToClipboard ()’ and ‘Paste ()’ code. This is very ‘dirty’: information is copied to the windows clipboard. (And yes, this is indeed the same clipboard that your ctrl-c and ctrl-v actions from any Windows program will use.)
(This is a translated version of my original article on this subject)
Stability issue
Office Interop, the technology under NetOffice and Microsoft.Office, uses a loosely coupled link between programs. This means that information can be exchanged, but without infringing on the memory protection of the relevant programs. However, due to this technique you suffer from timing and concurrency problems.
The Windows clipboard is also outside your program. Also with access to your clipboard you suffer from timing and concurrency problems.
Combined, copying slides creates great instability in your program.
For example, I received these errors:
- System.Runtime.InteropServices.COMException (0x80010105): The server threw an exception. (Exception from HRESULT: 0x80010105 (RPC_E_SERVERFAULT))
- System.Runtime.InteropServices.COMException (0x80010001): Call was rejected by callee. (Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))
Solution
The solution came to me in three parts:
- Use a stable Office Interop client
- Wait for the clipboard
- Catch Interop errors and try again
1 Stable Office Interop client
As this article already indicated, I switched to the NetOffice office automation solution. This library supplies you with the same call constructions as with Office Interop from Microsoft but it is slightly more stable.
This package can be downloaded with NuGet if you search on NetOffice.Powerpoint.
The official website for NetOffice is https://netoffice.codeplex.com/
(Pay attention: On the PC where you run the program you still need Microsoft Office. NetOffice Interop is only a connection between your program and Office.)
2 Wait for the clipboard
If you work with Office Interop, copying and pasting is done via the clipboard. However, these are wrapped COM calls.
Because of timing issues it could be that the information is not yet available on the clipboard on the moment you try to paste. To prevent getting COM errors, I use the following code.
using NetOffice.PowerPointApi; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace mppt.Connect { class ClipboardDemo { private _Presentation _presentatie; /// <summary> /// Insert a slide in the main presentation at the next position (main presentation was created when creating this class) /// </summary> /// <param name="slides">the slide that has to be inserted (the condition is that the presentation of which the slide is part is still open)</param> public void SlidesCopyToPresentation(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")); } } }
I will now briefly explain how and why this code works.
First the loop:
foreach (var slide in slides.ToList()) { slide.CopyToClipboard(); while (!HasClipboardPowerpointSlideContent()) { System.Threading.Thread.Sleep(100); } _presentatie.Slides.Paste(); }
Here the slides are copied individually.
After each copy action, a function checks whether something on the clipboard is related to Powerpoint. As long as that information is not there yet, it waits with Thread.Sleep ().
If the slide information is detected, the pasting can continue.
Next the function that polls whether something is on the clipboard:
private static bool HasClipboardPowerpointSlideContent() { var data = Clipboard.GetDataObject(); if (data == null) return false; var formats = data.GetFormats(); return formats.Any(f => f.StartsWith("PowerPoint")); }
The clipboard is in System.Windows.Forms. This clipboard supports all possible types of data. I am only interested if there is something and whether that comes from Powerpoint.
The GetFormats () indicates how you can interpret the information on the clipboard and how to convert it. With some trial-and-error I could deduce that, to detect Powerpoint content, the format had to start with ‘Powerpoint’.
By using the above method it is not completely sure that it is actually a Slide and not a Shape for example. And to add to that, the clipboard can also conflict with other copy actions that the user is making at the same time.
But in practice, this simple clipboard polling mechanism proves to be a reliable method of determining the succes of the CopyToClipboard() method.
3 Catch Interop errors
-and try again.
The previous code still has some problems. First of all, it happens regularly that a COM error occurs during the pasting. And besides that you run the risk of waiting indefinitely when polling the clipboard.
So we have to improve the code a bit:
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 SlidesCopyToPresentation(IEnumerable<IMppSlide> slides, int retryCount = 3) { var itemsMissed = 0; foreach (var slide in slides.ToList()) { slide.CopyToClipboard(); var success = false; for (int currentTry = 1; currentTry < retryCount && success == false; currentTry++) { try { success = ExecuteWhen(HasClipboardPowerpointSlideContent, () => { _presentatie.Slides.Paste(); }); } catch (System.Runtime.InteropServices.COMException) { } } if (!success) itemsMissed++; } return itemsMissed; } 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")); } } }
I will now briefly explain how and why this code works.
First a retry strategy:
var success = false; for (int currentTry = 1; currentTry < retryCount && success == false; currentTry++)
You can see that a ‘retry strategy’ has been placed around the combination of clipboard checking and pasting. A ‘retry strategy’ is indispensable if you work with a remote process that you do not have full control over.
And here the condition of success:
success = ExecuteWhen(HasClipboardPowerpointSlideContent, () => { _presentatie.Slides.Paste(); });
In short, this ExecuteWhen() is a retry-strategy around the first input parameter, the function HasClipboardPowerpointSlideContent(), and it will execute the delegate if the input function succeeds within the retry window.
ExecuteWhen() will return true when the delegate has been executed and false if the retry was unsuccessful.
Now the details of this ExecuteWhen() function:
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; }
You can use this function for more scenarios where you need a ‘retry strategy’.
First a minimal waiting time is issued (this is specified separate from the retry timing):
var waited = 0; if (minWaitTime > 0) System.Threading.Thread.Sleep(minWaitTime); waited += minWaitTime;
Only after this minimum waiting time will I will start checking the isTrue () function input parameter.
As you can see, I immediately keep track of the time I have waited in total so that I can stop after a maximum has been reached.
Next the loop that will check for either a timeout or the isTrue() function input parameter being positive.
while (waited < maxWaitTime && waitStep > 0 && !isTrue.Invoke())
And at last the action delegate invoking:
if (isTrue.Invoke()) { if (waitBeforeExecute > 0) System.Threading.Thread.Sleep(waitBeforeExecute); action.Invoke(); return true; }
We redo the isTrue () function input parameter check. This is because of two reasons. First we can arrive at this point without having checked the isTrue () function input parameter, but second we want to make sure we didn’t hit a false positive.
And, as icing on the cake, we introduce an extra waiting time here after detecting a good situation in order to be able to deal with programs that release a ready notification too early (which is plausible for a clipboard). It could be that the information is present but the originating copy is still pending.
Only when all this is done we will the execute the actual delegate. And return success.
Conclusion
You can find this code in my liturgy generator project on GitHub. The code is in this file.
https://stackoverflow.com/questions/8815895/why-is-thread-sleep-so-harmful
LikeLike
Well, this blog post is exactly about that: how to make the most of a badly designed component.
As the stackoverflow answers said
“We are waiting because some condition changes some time … keyword(s) is/are some time! if the condition-check is in our code-domain, we should use WaitHandles – otherwise the external component should provide some kind of hooks … if it doesn’t its design is bad!”
It is exactly that: the design of the COM Interop component is bad.
COM is asynchronous, so it should provide at least a wait handle with every operation… but it doesn’t.
And another bad design, if you ask me, is the use of the clipboard for inter proces communication (as in: really REALLY bad design).
But yeah, that’s how COM Interop works and that’s why Microsoft officially states that you shouldn’t use it outside of the ‘personal’ environments. https://support.microsoft.com/en-us/help/257757/considerations-for-server-side-automation-of-office
But the alternatives, to the use of the Offfice COM Interop, are very time consuming or very expensive. Pick your battles.
LikeLike