0

I have the most god-awful task of automating an excel workbook in C#. Part of the process is to call a macro in the workbook which takes around 2 minutes to execute. This all happens in a Windows service and happily runs from start to finish. At present it writes events to a database table just before calling the macro and when the macro has finished executing. It does a lot of calculations and exporting data to a text file within the macro code.

As it takes so long the users have asked if they can be notified at various parts of the process.

My initial thought with reservation was to periodically poll the Application.StatusBar which gets update whilst running the macro using System.Timers.Timer. I thought there might be some kind of thread issues with this - which I think is happening as the call from the timer to get the StatusBar does not return/complete for quite a large period of time (tens of seconds).

I have my workbook wrapped up in the following class which makes sure Excel closes correctly and runs the macro:

internal class myWorkbook : IDisposable
{
    private Microsoft.Office.Interop.Excel.Application app = null;
    private Microsoft.Office.Interop.Excel.Workbook myWorkbook = null;
    private string _myWorkbookUri;

    public myWorkbook(string myWorkbookUri, string)
    {
        _myWorkbookUri = myWorkbookUri;
    }

    public string Export(DateTime date)
    {
        app = new Microsoft.Office.Interop.Excel.Application();
        app.Visible = false;
        app.DisplayAlerts = false;
        app.Interactive = false;
        app.AskToUpdateLinks = false;
        myWorkbook = app.Workbooks.Open(_myWorkbookUri, 0, true, 5, "", "", false, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "", true, false, 0, true, true, false);

        return (string)app.Run("GenerateTextFile", date);       
    }

    /// <summary>
    /// Disposes the object instance and all unmanaged resources.
    /// </summary>
    void IDisposable.Dispose()
    {
        if (myWorkbook != null)
        {
            myWorkbook.Close(false);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(myWorkbook);
        }

        if (app != null)
        {
            app.Quit();
            System.Runtime.InteropServices.Marshal.ReleaseComObject(app);
        }
    }

    public string Status
    {
        get
        {
            if (myWorkbook == null)
                return string.Empty;
            else
            {   
                return myWorkbook.Application.StatusBar.ToString();
            }
        }
    }
}

Then I have tried to do the monitoring using the following nested/internal class in my report processing class:

private class MyMonitor : System.Timers.Timer
{
    private MyWorkbook _wb;
    private ReportGeneratorProcess _parent;
    private string _lastStatus = string.Empty;
    private bool handlingTimer = false;

    public MyMonitor(MyWorkbook wb, ReportGeneratorProcess parent)
    {
        _wb = wb;
        _parent = parent;
        this.AutoReset = true;
        this.Elapsed += new System.Timers.ElapsedEventHandler(this.timer_Elapsed);                
    }

    private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        if (_wb != null && !handlingTimer)
        {
            try
            {
                handlingTimer = true;
                string status = _wb.Status;
                if (status != _lastStatus && status.ToLower() != "false")
                    _parent.AddEvent(MSG_TITLE_RUN_My, status);

                _lastStatus = status;
            }
            finally
            {
                handlingTimer = false;
            }
        }
    }
}

This is all triggered by executing the following code in the ReportGeneratorProcess class (I've omitted the unnecessary code)

string outputFilename = null;
MyMonitor monitor = null;

try
{
    using (MyWorkbook wb = new MyWorkbook(_MyWorkbookUri))
    {
        monitor = new MyMonitor(wb, this);
        monitor.Start();
        outputFilename = wb.Export(month);
        monitor.Stop();
    }

    AddEvent("My Complete", "Generated file " + outputFilename);
    return outputFilename;
}
catch (Exception ex)
{
    AddEvent("", "failed");
    throw ex;
}
finally
{
    if (monitor != null)
    {
        monitor.Stop();
    }
}

AddEvent simply adds a new event to the database using the main class.

I remain defeated at the moment trying to think of an alternative solution/nice way around this. Anyone got any hints?

Unfortunately the process HAS to operate as is. There is no scope to move anything out of excel.

4

2 回答 2

1

我以前做过。创建一个数据库表来更新您长期运行的作业状态。您的应用程序发送请求以启动此作业,并应为此作业创建一个 Id。您的应用程序开始在计时器上监视此作业。您的 win 服务会接手这项工作并开始更新您的记录,您的应用程序现在将向用户显示此更新。您的 win 服务将句柄传递给 Excel 自动化,后者现在负责更新长期运行的作业记录。您的客户端应用程序将向用户反映更改。处理完成后,您的客户应采取相应的行动。一条消息、一个弹出窗口等将通知用户完成或失败的工作。

此外,许多开发人员创建了持续运行在计时器上的赢服务。一种更有效的方法是创建一个监听器。因此,win 服务将监听某个 TCP 端口,您的应用程序将向该端口发出信号。Win 服务将唤醒并开始处理作业。

于 2013-08-06T14:56:15.657 回答
0

基本上我稍微修改了工作簿以记录到文本文件。然后我的进程使用 FileSystemWatcher 监视文本文件。

工作簿在服务器本地创建日志文件。过去我在使用 FileSystemWatcher 时遇到过问题,它监控 unc 路径并且网络似乎出现故障,导致它停止工作,所以我相信这会没问题。

我认为 FileSystemWatcher 在单独的线程(未验证)上引发 Changed 事件,因为我的服务在计时器线程上运行 Export() 函数,该函数将被阻塞,直到 excel 宏完成执行。

事实证明,该服务正在愉快地运行并监视该文件。

internal class myWorkbook : IDisposable
{
    private Microsoft.Office.Interop.Excel.Application app = null;
    private Microsoft.Office.Interop.Excel.Workbook myWorkbook = null;
    private string _myWorkbookUri;
    private FileSystemWatcher watcher;
                private bool _visible;
                private string _logFile;

    public myWorkbook(string myWorkbookUri, string)
    {
        _myWorkbookUri = myWorkbookUri;
    }

    public string Export(DateTime date)
    {
        app = new Microsoft.Office.Interop.Excel.Application();
        app.Visible = false;
        app.DisplayAlerts = false;
        app.Interactive = false;
        app.AskToUpdateLinks = false;
        myWorkbook = app.Workbooks.Open(_myWorkbookUri, 0, true, 5, "", "", false, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "", true, false, 0, true, true, false);
        CreateWatcher();

        return (string)app.Run("GenerateTextFile", date);       
    }


    private void CreateWatcher()
    {
        watcher = new FileSystemWatcher();
        watcher.Path = Path.GetDirectoryName(_logFile);
        watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
        watcher.Filter = "*.txt"; // could use _logFile;
        watcher.Changed += watcher_Changed;
        watcher.EnableRaisingEvents = true;
    }

    void watcher_Changed(object sender, FileSystemEventArgs e)
    {
        if (e.FullPath == _logFile)
        {
            using (FileStream fs = new FileStream(_logFile,
                                  FileMode.Open,
                                  FileAccess.Read,
                                  FileShare.ReadWrite))
            {
                using (StreamReader sr = new StreamReader(fs))
                {
                    while (sr.Peek() >= 0)
                    {
                        var line = sr.ReadLine();
                        var tabs = line.Split(new char[] { '\t' });
                        DateTime eventTime = DateTime.Parse(tabs[0]);

                        if (DateTime.Compare(eventTime, lastEventTime) > 0)
                        {
                            string eventMessage = tabs[1];
                            OnProgressChanged(new ProgressChangedEventArgs(eventTime, eventMessage));

                            lastEventTime = eventTime;
                        }
                    }
                }
            }
        }
    }

}
于 2013-08-09T07:33:09.010 回答