【问题标题】:WebBrowser Control Slideshow Hanging after Running for Extended TimeWebBrowser 控制幻灯片在长时间运行后挂起
【发布时间】:2015-10-07 18:03:24
【问题描述】:

我是一个相当新的开发人员,这个让我很难过。

我的 WinForms 应用程序是用于网站的幻灯片,它通过 URL 列表旋转,通过使用第二个表单作为“窗帘”在每次转换时淡入/淡出。它的意思是无限期运行,但在运行几天后一直挂在过渡上。

表格1:

HttpWebResponse response = null;
List<Slide.Doc> sList = null;

bool repeatSlideshow = true;
bool pageLoaded = false;

double curtainAnimStep = 0.05;
int errorCount = 0;

public Form1()
{
    InitializeComponent();

    CursorShown = false;

    this.Visible = true;
    this.FormBorderStyle = FormBorderStyle.None;
    this.WindowState = FormWindowState.Maximized;

    webBrowser1.ScrollBarsEnabled = false;
    webBrowser1.ScriptErrorsSuppressed = true;

    Slideshow(environment, channel);
}


public void Slideshow(string environment, string channel)
    {           
        while (repeatSlideshow)
        {
            try
            {
                sList = Slide.convertJSONToSlide(Slide.getParams(environment, channel));
            }
            catch (Exception)
            {
                Form2 curtain = new Form2(curtainAnimStep);
                curtain.Show();
                waitForFade(curtain, 1);
                displayError();
                raiseCurtain(curtain, curtainAnimStep);
                waitForFade(curtain, 0);
                curtain.Dispose();
                waitAround(30);
                continue;
            }

            foreach (Slide.Doc s in sList)
            { 
                bool slideWasDisplayed = false;

                Form2 curtain = new Form2(curtainAnimStep);
                curtain.Show();
                waitForFade(curtain, 1);
                slideWasDisplayed = displaySlide(s.URL_TEXT);
                if (slideWasDisplayed == false)
                {
                    webBrowser1.DocumentText = "<html><body style='background-color: #1C1C1C;'></body></html>";
                    redrawPage();
                }
                raiseCurtain(curtain, curtainAnimStep);
                waitForFade(curtain, 0);
                curtain.Dispose();
                if (slideWasDisplayed == true)
                {
                    waitAround(s.DISPLAY_SEC);
                }

            }

            if (errorCount == sList.Count)
            {
                Form2 curtain = new Form2(curtainAnimStep);
                curtain.Show();
                waitForFade(curtain, 1);
                displayError();
                raiseCurtain(curtain, curtainAnimStep);
                waitForFade(curtain, 0);
                curtain.Dispose();
                waitAround(30);                  
            }

            errorCount = 0;

            Utilities.Web.WebBrowserHelper.WebBrowserHelper.ClearCache();
        }
    }

    public bool displaySlide(string slideUrl)
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(slideUrl);
        request.Timeout = 1000;
        try
        {
            response = (HttpWebResponse)request.GetResponse();
            webBrowser1.Navigate(slideUrl);
            redrawPage();
            response.Dispose();
            return true;
        }
        catch (WebException)
        {
            errorCount++;
            return false;
        }
    }

    public void redrawPage()
    {
        while (pageLoaded == false)
        {
            Application.DoEvents();
        }
        webBrowser1.Invalidate();
        Application.DoEvents();
        pageLoaded = false;
    }

    public void raiseCurtain(Form curtain, double curtainAnimStep)
    {
        while (curtain.Opacity > 0)
        {
            curtain.Opacity -= curtainAnimStep;
            Application.DoEvents();
            System.Threading.Thread.Sleep(10); // How long between shifts in opacity (NOT interval between slides)
        }
    }

    public void waitAround(int duration)
    {
        DateTime dt2 = DateTime.Now;
        while (dt2.AddSeconds(duration) > DateTime.Now)
        {
            Application.DoEvents();
        }
    }

    public void waitForFade(Form curtain, int finalOpacity)
    {
        while (curtain.Opacity != finalOpacity)
        {
            DateTime dt = DateTime.Now;
            dt = dt.AddSeconds(1);
            while (dt > DateTime.Now)
            {
                Application.DoEvents();
            }
        }
    }

    private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
    {
        pageLoaded = true;
    }

表格2:

public Form2(double animStep)
        {
            InitializeComponent();
            this.AnimStep = animStep;
        }

        public double AnimStep { get; set; }

        private async void Form2_Load(object sender, EventArgs e)
        {
            while (Opacity < 1.0)
            {
                await Task.Delay(10);
                Opacity += AnimStep;
            }
            Opacity = 1;
        }

我已经为此工作了很长时间,但我不得不承认我真的不知道此时我应该寻找什么。

Application.DoEvents的使用能负责吗?将它们排除在外会破坏应用程序,但我无法找到替代方法。

【问题讨论】:

  • DoEvents 在紧密循环上是一种代码气味,它可能会导致意外的重入,并且可能是您的问题的根源。请参阅我的评论here

标签: c# .net winforms webbrowser-control slideshow


【解决方案1】:

查看您的代码(如 Noseratio 所示),我建议的一件事是摆脱对 DoEvents 调用的需要。请记住,在 Windows 中,有一个专用的 UI 线程用于更新窗体上的控件。当您在同一个 UI 线程上做很多事情(在循环中,调用一堆方法)时,Windows 控件依赖于您的合作以与它们共享一些时间,因此调用DoEvents

我将使用BackgroundWorkerTimerWaitHandle 来安排将从后台线程更新 UI 的命令。这样我们就可以在 UI 线程上做尽可能少的事情了。

表单加载

Form1 将只有一个 webbrowsercontrol 和一个 backgroundworker。队列将保存需要执行的命令。我们从 Load 事件启动 Backgroundworker。

    Form2 frm2 = new Form2();
    Queue<ICommandExecutor> commands = new Queue<ICommandExecutor>();

    private void Form1_Load(object sender, EventArgs e)
    {
        frm2.Show();
        frm2.BringToFront();
        commands.Enqueue(new LoadSlideShow(this, frm2, commands));
        backgroundWorker1.RunWorkerAsync();
    }

后台工作人员

Backgroundworker DoWork 事件是在它自己的后台线程上运行的引擎。只要在队列中找到命令,它就会运行。获取命令后,它的 Execute 方法被触发。如果命令支持处理Dispose 方法,则调用该方法并处理命令并重新开始。

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {                
        while(commands.Count>0)
        {
            ICommandExecutor cmd = commands.Dequeue();
            try
            {
                cmd.Execute();
                // dispose if we can
                IDisposable sync = cmd as IDisposable;
                if (sync != null)
                {
                    sync.Dispose();
                }
            }
            catch(Exception exp)
            {
                // add commands here
                Trace.WriteLine("error" + exp.Message);
            }
        }   
    }

命令

有一个标准接口可用于实现命令模式。 ICommandExecutor 有一个方法,Execute。我们可以创建不同的类来实现这个接口。每个类都有自己的状态和引用,它可以像一个计时器一样简单,也可以像加载一批新的要显示的 URL 一样复杂。

    public class ShowSlide:ICommandExecutor
    {
        string url;
        Form1 form;
        AutoResetEvent done = new AutoResetEvent(false);
        public ShowSlide(Form1 form, string url)
        {
            this.url = url;
            this.form = form;
        }

        public void Execute()
        {
            // if we are not on the UI thread...
            if (form.InvokeRequired)
            {
                // ... switch to it...
                form.Invoke(new MethodInvoker(Execute));
            }
            else
            {
                // .. we are on the UI thread now
                // reused from your code
                form.displaySlide(url);
            }
        }
    }

这是一个计时器。请注意如何使用 Timer 类和 timerDone 等待句柄以使后台线程仅在调用 Dispose 时定时器完成时继续工作。

    public class WaitForSeconds: ICommandExecutor, IDisposable
    {
        int ms;
        System.Threading.Timer timer;
        ManualResetEvent timerDone = new ManualResetEvent(false);
        public WaitForSeconds(int secs)
        {
            this.ms = secs * 1000;
        }

        public void Execute()
        {
            // use a timer
            timer = new System.Threading.Timer(
               (state) => timerDone.Set() // signal we are done
               );
            timerDone.Reset();
            timer.Change(this.ms, Timeout.Infinite);
        }

        public void Dispose()
        {
            timerDone.WaitOne();
            timerDone.Dispose();
            timer.Dispose();
        }
    }

为了以正确的顺序设置命令,我们使用以下命令类实现,它将命令队列、Form1 和 Form2 作为其构造函数的参数。 Execute 命令加载所有 url 以提供给 webbrowser 控件。对于每个 url,它将需要执行的命令添加到队列中。最后,this 实例也被添加到队列中,这意味着如果所有命令都已处理,则将再次使用该类。队列永远不会是空的。

    public class LoadSlideShow: ICommandExecutor
    {
        readonly Queue<ICommandExecutor> commands;
        readonly Form1 form;
        readonly Form2 form2;
        public LoadSlideShow(Form1 form, Form2 form2, Queue<ICommandExecutor> cmds)
        {
            this.form = form;
            commands = cmds;
            this.form2 = form2;
        }

        public void Execute()
        {
            var list = Slide.convertJSONToSlide(null);
            foreach (var slide in list)
            {
                commands.Enqueue(new ShowSlide(form, slide.URL_TEXT));
                commands.Enqueue(new WaitForSeconds(1));
                //commands.Enqueue(new LowerCurtain(form2));
                commands.Enqueue(new WaitForSeconds(slide.DISPLAY_SEC));
                //commands.Enqueue(new RaiseCurtain(form2));
            }
            commands.Enqueue(this); 
        }
    }

这基本上就是制作基本幻灯片所需的全部内容。

对于所谓的窗帘,我们将使用 Form2 做类似的事情,但我也会使用 BackgroundWorker_progress 事件。

Form2 窗帘

Form2 将通过在循环中更改 Opacity 来充当窗帘。它有自己的后台工作人员:

    ManualResetEvent stateChange = new ManualResetEvent(false);
    public ManualResetEvent stateChangeDone = new ManualResetEvent(false);

    private void Form2_Load(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();  
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        while(stateChange.WaitOne())
        {
            stateChange.Reset();
            var progressDone = new AutoResetEvent(false);
            int progress = 0;
            using(var timer = new System.Threading.Timer(_=>
                { 
                    backgroundWorker1.ReportProgress(progress);
                    progress += 2;
                    if (progress>=100)
                    {
                        progressDone.Set();
                    }
                }, null, 0, 25))
            {
                progressDone.WaitOne();
            }
            stateChangeDone.Set();
        }
    }

后台工作人员调用 ResportProgress 并使用一个 int 指示其 prpgress。这会引发 ProgressChanged 事件。根据 Curtain 需要处于什么状态,我们计算出Opacity 的正确值。

    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        switch(state)
        {
            case Curtain.Up:
                this.Opacity = e.ProgressPercentage / 100.0;
                break;
            case Curtain.Down:
                this.Opacity = (100 - e.ProgressPercentage) / 100.0;
                break;
        }
    }

为了开始这一切,我们创建了两个名为 Up 和 Down 的公共方法:

    enum Curtain
    {
        Up,
        Down
    }

    Curtain state;

    public void Up()
    {
        state = Curtain.Up;
        stateChange.Set();
        stateChangeDone.Reset();
    }

    public void Down()
    {
        state = Curtain.Down;
        stateChange.Set();
        stateChangeDone.Reset();
    }

这样我们就只剩下命令类的实现了,这些命令类将被添加到命令队列并由 Form1 的后台工作人员处理:

    public class RaiseCurtain:ICommandExecutor, IDisposable
    {
        readonly Form2 form2;

        public RaiseCurtain( Form2 form2)
        {
            this.form2 = form2;
        }

        public void Execute()
        {
            if (form2.InvokeRequired)
            {
                form2.Invoke(new MethodInvoker(Execute));
            }
            else
            {
                form2.BringToFront();
                form2.Up();
            }
        }

        public void Dispose()
        {
            form2.stateChangeDone.WaitOne();
        }
    }

    public class LowerCurtain : ICommandExecutor,IDisposable
    {
        readonly Form2 form2;

        public LowerCurtain(Form2 form2)
        {
            this.form2 = form2;
        }

        public void Execute()
        {
            if (form2.InvokeRequired)
            {
                form2.Invoke(new MethodInvoker(Execute));
            }
            else
            {
                form2.Down();
            }
        }

        public void Dispose()
        {
            form2.stateChangeDone.WaitOne();
        }
    }

就是这样。我们已经消除了 DoEvents 的使用。

有一个警告:这并不能保证应用程序会在几个小时/几天后再次停止。原因是possible memory-leak in the webbrowser control,在我的测试中我确实看到了同样的效果,私人内存消耗缓慢但稳定地增加,而托管内存字节几乎保持不变。

由于没有任何帖子提供明确的答案,因此一种选择可能是将您的应用程序重新启动为 indicates in one of the answers here。从好的方面来说,您现在可以将其实现为 Command 类...

【讨论】:

    猜你喜欢
    • 2018-02-08
    • 2013-09-05
    • 2011-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多