PowerPoint Interop: How to detect when the user closes a PowerPoint without saving their changes?

435 views Asked by At

I have a WPF application that stores PowerPoint files in a SQL Server database. The application has an edit button that opens a given PowerPoint file for editing. Since PowerPoint is a file-based application, I must use temporary files to load and save the PowerPoints.

The helper class I've written for this purpose has a property on it that binds to the edit button's IsEnabled property; we disable the button while an edit is in progress. There is a ManualResetEvent that suspends the Edit method in this helper class while the edit is in progress. I hook the Presentation.Saved event to detect when the user has saved changes to their PowerPoint. Needless to say, this is all carefully orchestrated.

During testing, we discovered that, if the user closes the PowerPoint without saving their changes, and then answers "yes" to the "do you wish to save your changes" dialog that follows, Presentation.Saved does not fire until after the Presentation.CloseFinal event has already executed, which is too late for us to do anything about it. Presentation.CloseFinal is where we retrieve the saved file from the disk and store it to the database. Presentation.Saved fires immediately if the user clicks the Save button in PowerPoint.

In an effort to solve the problem, I hooked the PresentationClose event and wrote the following code.

// Detects when the user closes the PowerPoint after changes have been made.
private void Application_PresentationClose(Presentation presentation)
{
    // If the user has edited the presentation before closing PowerPoint,
    // this event fires twice.  The first time it fires, presentationSaved
    // is msoFalse.  That's how we know the user has edited the PowerPoint.
    if (presentation.Saved == MsoTriState.msoFalse)
        IsDirty = true;
}

I then check my IsDirty property in Presentation.CloseFinal and save the PowerPoint to the database if it is set to true.

Unfortunately there's an unforeseen wrinkle; there doesn't seem to be a reliable way to determine whether or not the user abandoned their changes. The second time this event fires, the Presentation.Saved property is always set to MsoTriState.msoTrue, regardless of the answer the user gave to the "save your changes?" dialog.

Is there a way in PowerPoint Interop to avoid taking the cost of saving an unchanged file to the database, if the user abandoned their changes?


For reference, here is the helper class, in its entirety:

using Microsoft.Office.Core;
using Microsoft.Office.Interop.PowerPoint;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Xps.Packaging;
using PropertyChanged;  // PropertyChanged.Fody 

namespace Helpers
{
    /// <summary>
    /// Helper class for PowerPoint file handling
    /// </summary>
    [AddINotifyPropertyChangedInterface]
    public sealed class PowerPointApplication : IDisposable
    {
        private Application _application;
        private Presentation _presentation;
        private string _tempFolder;
        private string _pptPath;
        private string _extension;

        /// <summary>
        /// Used to block the Edit method until the PowerPoint presentation is closed.
        /// </summary>
        private ManualResetEvent manualResetEvent = new ManualResetEvent(false);

        public byte[] PptData { get; private set; }
        public byte[] PptxData { get; private set; }
        public byte[] JpgData { get; private set; }

        /// <summary>
        /// Indicates whether any instance of PowerPointApplication is in an edit state.
        /// Used to disable Edit PowerPoint buttons.
        /// </summary>
        public static bool IsEditing { get; private set; }

        /// <summary>
        /// Indicates if the PowerPoint file has been saved after changes were made to it.
        /// </summary>
        public bool IsSaved { get; set; }

        /// <summary>
        /// Indicates if the PowerPoint file has been changed but not saved.
        /// </summary>
        public bool IsDirty { get; set; }

        public PowerPointApplication()
        {
            _tempFolder = Path.GetTempPath();

            if (!Directory.Exists(_tempFolder))
                Directory.CreateDirectory(_tempFolder);

            _application = new Application();
            _application.PresentationSave += Application_PresentationSave;
            _application.PresentationClose += Application_PresentationClose;
            _application.PresentationCloseFinal += Application_PresentationCloseFinal;
        }

        // Detects when the user presses the "Save" button in PowerPoint
        private void Application_PresentationSave(Presentation presentation)
        {
            IsSaved = true;
        }

        // Detects when the user closes the PowerPoint after changes have been made.
        private void Application_PresentationClose(Presentation presentation)
        {
            // If the user has edited the presentation before closing PowerPoint,
            // this event fires twice.  The first time it fires, presentationSaved
            // is msoFalse.  That's how we know the user has edited the PowerPoint.
            // It fires again after the users has responded to the "save changes?" dialog.
            if (presentation.Saved == MsoTriState.msoFalse)
                IsDirty = true;
        }

        private void Application_PresentationCloseFinal(Presentation presentation)
        {
            if ((IsDirty || IsSaved) && File.Exists(_pptPath))
            {
                var data = File.ReadAllBytes(_pptPath);

                if (_extension == "pptx")
                {
                    PptxData = data;
                    PptData = GetPpt(presentation);
                }
                else
                {
                    PptData = data;
                    PptxData = GetPptx(presentation);
                }
                JpgData = GetJpg(presentation);

                IsSaved = true;
                IsDirty = false;
            }
            manualResetEvent.Set();
            IsEditing = false;

            Task.Run(() => DeleteFileDelayed(_pptPath));
        }

        /// <summary>
        /// Waits for PowerPoint to close, and then makes a best effort to delete the temp file.
        /// </summary>
        private static void DeleteFileDelayed(string path)
        {
            if (path == null) return;
            var file = new FileInfo(path);

            Thread.Sleep(5000);
            try
            {
                file.Delete();
            }
            catch { }
        }

        /// <summary>
        /// Opens the provided PowerPoint byte array in PowerPoint and displays it.
        /// </summary>
        public void Edit(byte[] data, string ext = "xml")
        {
            _extension = ext;
            _pptPath = GetTempFile(_extension);

            if (data == null)
            {
                // Open a blank presentation and establish a save path.
                _presentation = _application.Presentations.Add(MsoTriState.msoTrue);
                _presentation.SaveAs(_pptPath, PpSaveAsFileType.ppSaveAsXMLPresentation);
                IsSaved = false;
            }
            else
            {
                // Save the data to a file and open it.
                File.WriteAllBytes(_pptPath, data);
                _presentation = _application.Presentations.Open(_pptPath);
                IsEditing = true;
            }

            // Make sure IsEnabled state of WPF buttons is properly set.
            ApplicationHelper.DoEvents();
            Thread.Sleep(100);
            ApplicationHelper.DoEvents();

            // Wait for PowerPoint to exit.
            manualResetEvent.WaitOne();
        }

        /// <summary>
        /// Opens the provided PowerPoint byte array in PowerPoint without displaying it.
        /// </summary>
        public void Open(byte[] data, string ext = "xml")
        {
            _extension = ext;
            _pptPath = GetTempFile(ext);
            File.WriteAllBytes(_pptPath, data);
            _presentation = _application.Presentations.Open(_pptPath, WithWindow: MsoTriState.msoFalse);
            IsEditing = true;
        }

        public void Close()
        {
            _presentation.Close();
        }

        public byte[] GetJpg() { return GetJpg(_presentation); }
        /// <summary>
        /// Returns a byte array containing a JPEG image of the first slide in the PowerPoint.
        /// </summary>
        public byte[] GetJpg(Presentation presentation)
        {
            presentation.SaveCopyAs(_tempFolder, PpSaveAsFileType.ppSaveAsJPG, MsoTriState.msoFalse);
            byte[] result = File.ReadAllBytes(Path.Combine(_tempFolder, "Slide1.jpg"));
            File.Delete(Path.Combine(_tempFolder, "Slide1.jpg"));
            return result;
        }

        public byte[] GetPptx() { return GetPptx(_presentation); }

        public byte[] GetPptx(Presentation presentation)
        {
            var path = Path.ChangeExtension(_pptPath, "pptx");
            presentation.SaveCopyAs(path, PpSaveAsFileType.ppSaveAsOpenXMLPresentation, MsoTriState.msoFalse);
            byte[] result = File.ReadAllBytes(path);
            return result;
        }

        public byte[] GetPpt(Presentation presentation)
        {
            var path = Path.ChangeExtension(_pptPath, "ppt");
            presentation.SaveCopyAs(path, PpSaveAsFileType.ppSaveAsPresentation, MsoTriState.msoFalse);
            byte[] result = File.ReadAllBytes(path);
            return result;
        }

        /// <summary>
        /// Returns an XPS document of the presentation.
        /// </summary>
        public XpsDocument ToXps(string pptFilename, string xpsFilename)
        {
            var presentation = _application.Presentations.Open(pptFilename, MsoTriState.msoTrue, MsoTriState.msoFalse, MsoTriState.msoFalse);
            presentation.ExportAsFixedFormat(xpsFilename, PpFixedFormatType.ppFixedFormatTypeXPS);
            return new XpsDocument(xpsFilename, FileAccess.Read);
        }

        /// <summary>
        /// Returns a path to a temporary working file having the specified extension.
        /// </summary>
        private string GetTempFile(string extension)
        {
            return Path.Combine(_tempFolder, Guid.NewGuid() + "." + extension);
        }

        #region IDisposable implementation
        public void Dispose()
        {
            _application.PresentationSave -= Application_PresentationSave;
            _application.PresentationClose -= Application_PresentationClose;
            _application.PresentationCloseFinal -= Application_PresentationCloseFinal;

            IsEditing = false;
        }
        #endregion
    }
}
0

There are 0 answers