簡體   English   中英

C# 字符串變空

[英]C# string becoming empty

C# 和一般編碼相對較新(這里的第一篇文章)。 我有一個 WinForms 本地應用程序,其中一些信息在 ReadOnly(true) RichTextBox 中顯示給用戶。 幾乎我所有的課程都需要向那個 RichTextBox 發送信息。 為了簡化此過程,我在 static class 中創建了一個方法,該方法使用鎖定的委托將信息發送到該 RichTextBox。 這是一個示例:

static class MyClass
{
    public delegate void MessageReceivedEventHandler(string message);
    public static event MessageReceivedEventHandler messageReceivedEventHandler;

    public static void MessageBox(string message)
    {
        lock (messageReceivedEventHandler)
        {
            //Thread.Sleep(20);
            messageReceivedEventHandler?.Invoke(message);
        }
    }
}
partial class MyForm : Form
{
    public MyForm()
    {
        MyClass.messageReceivedEventHandler += OnMessageReceived;
    }

    private void OnMessageReceived(string message)
    {
        richTextBox1.Text = richTextBox1.Text.Insert(0, $" {message}\n");
    }
    private void Button1_click()
    {
        MyClass.MessageBox("This should be working!");
        //Add more work here...
    }
}

上面的代碼只會打印“這應該可以工作”。 在 RichtTextbox 內。

問題是來自richTextBox1 的文本有時會變空。 當快速連續調用 MessageBox 方法時,似乎會出現此問題。 我的假設是,由於我有不同的任務同時運行(在我的代碼的其他部分),可能是兩個任務試圖使用相同的 static 資源,因此使用了 Lock。 但我仍然有這個問題。

添加 Thread.Sleep(20) 似乎可以解決問題,但這遠非優雅/強大。 當睡眠時間小於 10 毫秒時,它再次開始分解。

編輯1:為了澄清我所說的“字符串變空”的意思,這意味着來自richTextBox1的文本在某些時候是==“”,這不應該發生,因為代碼總是插入文本,而不是替換它。 OnMessageReceived 方法是對 RichTextBox 文本執行操作的唯一位置。

編輯 2:我看到許多與正在運行的其他任務相關的問題。 首先,是的,它是一個多線程應用程序。 這些任務和我的主要形式之間的唯一關系是我在上面寫的“打印”function。 為了提供更多上下文,此應用程序用於控制相對於電信號的步進電機的 position。 這樣做時,我需要在主窗體中打印重要信息。 這就是為什么在我的 RichTextBox(我打印信息的地方)中丟失信息是一個問題。 我丟失 RichTextBox 中的文本的可能原因應該是這個線程的焦點。

請記住,這是一個個人項目,而不是大規模應用程序。

謝謝,勞倫特

您的代碼中有多個問題。

首先,您不應該鎖定公共 object,因為這允許其他線程鎖定相同的 object,從而有可能使您的線程互鎖。 其次,您的症狀表明多個線程正在嘗試訪問資源。 與其依賴復雜的線程鎖定代碼,不如在 UI 上下文上安排 UI 操作,這將允許從后台任務調用添加消息。

最好的方法是使用Control.BeginInvoke()

你不能到處復制你的表單實例,所以我們將公開一個 static 方法。 您可以將 class 設為 singleton,但如果您需要多個無法工作的實例。 我將舉一個更通用的例子。 當調用 static 方法時,您將無法再訪問表單實例,因此我們將使用帶有事件和委托的 IOC 模式。

讓我們創建一個私有 static 事件,所有實例都將在構造函數中注冊回調。 當 static 方法引發 static 事件時,將調用所有實例回調。 回調將安排對其文本框的修改。

partial class MyForm : Form
{
    private class MessageWriteRequestedEventArgs : EventArgs
    {
        public string Message { get; }
        public MessageWriteRequestedEventArgs(string message)
        {
            Message = message;
        }
    }

    private static event EventHandler<MessageWriteRequestedEventArgs> MessageWriteRequested;

    public MyForm()
    {
        MessageWriteRequested += OnMessageWriteRequested;
    }

    public static void WriteMessage(string message)
    {
        MessageWriteRequested?.Invoke(this, new MessageWriteRequestedEventArgs(message));
    }

    private void OnMessageWriteRequested(object sender, MessageWriteRequestedEventArgs e)
    {
        richTextBox1.BeginInvoke(() => WriteMessageSafe(e.message));            
    }

    private void WriteMessageSafe(string message)
    {
        richTextBox1.Text = richTextBox1.Text.Insert(0, $" {message}\n");
    }

    private void Button1_click()
    {
        // you're on ui context, you're safe to access local ui resources
        WriteMessageSafe("This should be working!");
        // if you have multiple MyForm instances, you need to use the event
        WriteMessage("Broadcasting my tralala");
    }
}

如果您需要從其他任何地方寫入文本框:

// do stuff
MyForm.WriteMessage("Ho Ho Ho !");

.NET 已經包含一個 class 用於以線程安全的方式報告異步操作的進度(或任何其他信息) Progress< T> 它不需要鎖定,甚至更好的是,它將發送者和接收者解耦。 許多長時間運行的 BCL 操作接受IProgress<T>參數來報告進度。

您還沒有解釋表單中發生了什么,或者報告數據的任務是什么。 假設生產者是相同形式的另一個方法,您可以在啟動異步操作的相同方法中創建一個Progress<T>實例,例如:

async void Button1_Click()
{
    var progress=new Progress<string>(ReportMessage);

    ReportMessage("Starting");
    await Task.Run(()=>SomeLongOp(progress));
    ReportMessage("Finished");
}

void SomeLongOp(IProgress<string> progress)
{

    for(int i=0;i<1000000;i++)
    {
        ...
        progress.Report($"Message {i}");
        ...
    }

}

void ReportMessage(string message)
{
    richTextBox1.Text = richTextBox1.Text.Insert(0, $" {message}\n");
}

通過使用IProgress< T>SomeLongOp方法不會綁定到特定表單或全局實例。 它很容易成為另一個 class 的方法

發布大量消息

假設您有很多工人,做很多事情,例如監控很多設備,並希望所有設備都將消息發布到同一個Log文本框或 RTF 框。 Progress< T> “簡單地”在其原始同步上下文中執行報告委托或事件處理程序。 它沒有異步Report方法,也不能對消息進行排隊。 在真正高流量的環境中,同步切換會延遲所有工作人員。

對此的內置答案是使用 pub/sub 類之一,如ActionBlock< T>Channel

ActionBlock<T>按順序處理其輸入隊列中的消息,默認情況下使用在 ThreadPool 上運行的工作任務。 這可以通過在其執行選項中指定不同的TaskScheduler來更改。 默認情況下,它的輸入隊列是無界的。

可以使用ActionBlock接收來自多個工作人員的消息並將它們顯示在文本框上。 該塊可以在構造函數中創建,並作為ITargetBlock<T>接口傳遞給所有工作人員:


ActionBlock<string> _logBlock;

public MyForm()
{

    var options=new ExecutionDataFlowBlockOptions {
        TaskScheduler=TaskScheduler.FromCurrentSynchronizationContext();
    };
    _block=new ActionBlock<string>(ReportMessage,options);

}

現在樂趣開始了。 如果工人是由表單本身創建的,工人可以直接發布到塊:

public async void Start100Workers_Click(...)
{
    var workers=Enumerable.Range(0,100)
                          .Select(id=>DoWork(id,_block));
    await Task.WhenAll(workers);
}

async Task DoWork(int id,ITargetBlock<string> logBlock)
{
    .....
    await logBlock.SendAsync(message);
    ...
}

或者該塊可以通過公共屬性公開,因此應用程序中的其他類/表單可以發布到它。

public ITargetBlock<string> LogBlock=>_block;

我將展示一種簡單的方法來做我認為你所追求的事情。

我從 .NET Core 3.1 Win forms 應用程序開始。 我在表單中添加了一個富文本控件。 我在表單中添加了一個按鈕。

我添加了一個 TaskCompletionSource 作為實例屬性 - 這將用於控制充當您描述的工作人員的任務。

CancellationTokenSource sharedCancel = new CancellationTokenSource();

我創建了一個接口來表示接受您所描述的消息的東西:

public interface IMyMessageSink
{
    Task ReceiveMessage(string message);
}

我讓我的表單支持這個接口。

public partial class Form1 : Form, IMyMessageSink

ReceiveMessage 方法如下所示:

    public Task ReceiveMessage(string message)
    {
        if(this.sharedCancel == null || this.sharedCancel.IsCancellationRequested)
            return Task.FromResult(0);

        this.Invoke(new Action<Form1>((s) => this.richTextBox1.Text = this.richTextBox1.Text.Insert(0, $"{message}\n")), this);

        return Task.FromResult(0);
    }

您將看到 Invoke 將同步處理回 UI 線程。

這可能應該使用 BeginInvoke,然后將 APM 轉換為您可以在此處閱讀的異步任務。 但是對於一個 SO 答案,上面的簡單代碼就足夠了。

另請注意,沒有錯誤處理。 您需要將其添加到您的生成器和按鈕處理程序中。

接下來我創建了一個 class 來表示創建消息的東西。 這個 class 采用創建的接口和取消令牌。 它看起來像這樣:

public class MyMessageGenerator
{
    CancellationToken cancel;
    IMyMessageSink sink;

    public MyMessageGenerator(CancellationToken cancel, IMyMessageSink sink)
    {
        this.cancel = cancel;
        this.sink = sink;
    }

    public async Task GenerateUntilCanceled()
    {
        try
        {
            while (!this.cancel.IsCancellationRequested)
            {
                await sink.ReceiveMessage(this.GetHashCode().ToString());
                await Task.Delay(5000, this.cancel);
            }
        }
        catch (OperationCanceledException)
        { }
    }
}

在按鈕處理程序中,我們創建消息生成器。

    async void button1_Click(object sender, EventArgs e)
    {
        if (null == this.sharedCancel)
            return;

        await Task.Run(() => new MyMessageGenerator(this.sharedCancel.Token, this).GenerateUntilCanceled());
    }

最后我為表單關閉事件添加了一個覆蓋:

    protected override void OnClosing(CancelEventArgs e)
    {
        if (null != this.sharedCancel)
        {
            this.sharedCancel.Cancel();
            this.sharedCancel.Dispose();
            this.sharedCancel = null;
        }

        base.OnClosing(e);
    }

如果應用程序變得更大更復雜,您可能會通過添加使用 DI 容器公開的服務而受益。 您可以在此處閱讀有關將 DI 添加到 winforms 應用程序的信息。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM