You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI中TCP监听器正常但CollectionView UI不更新求助

MAUI中TCP监听实时更新CollectionView界面冻结问题

我是MAUI新手,遇到TCP监听器相关问题:监听器本身工作正常,但UI中的CollectionView无法实时显示信号,界面处于冻结状态。当点击CollectionView时会显示缓存的信号,之后又停止更新,直到选中行发生变化。我尝试过使用async Task和Thread,但都未解决问题,请问我哪里出错了?

原C#代码

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Xml;
using Microsoft.Maui.Controls;
using Npgsql;

namespace Black_Program;

public partial class SignalsPage : ContentPage
{
    public static bool searchRedy = true;
    public static int daysNotPayedAlarm = 60;
    System.Threading.Thread ReadTerminalSignals;

    public SignalsPage()
    {
        InitializeComponent();

        Task.Run(async () => await ReadFromIp());
    }

    async Task ReadFromIp()
    {
        var terminalSignalsList = new List<string>();
        int terminalSignalsCount = 0;
        TcpClient tcpclnt = new TcpClient();
        try
        {
            while (true)
            {
                try
                {
                    tcpclnt.Connect(MainPage.sqlServerIP, MainPage.sqlServerTerminalPort);
                }
                catch (Exception)
                {
                }
                while (tcpclnt.Connected)
                {
                    Thread.Sleep(10);
                    string messege = string.Empty;
                    Stream stream = tcpclnt.GetStream();
                    byte[] bytes = new byte[1024];
                    int length = stream.Read(bytes, 0, 1024);

                    /* Convert from ascii to stream */
                    for (int i = 0; i < length; i++)
                        messege += (Convert.ToChar(bytes[i]));

                    //byte[] utfBytes = Encoding.Default.GetBytes(messege);
                    messege = Encoding.UTF8.GetString(bytes);
                    try
                    {
                        ////Update new signal if object selected
                        string[] newMessages = messege
                            .Split(new char[] { '-' }
                            , StringSplitOptions.RemoveEmptyEntries)
                            .ToArray();
                        if (signals_number_search.Text == newMessages[0].TrimEnd())
                        {
                            string numberData = signals_number_search.Text.Replace(" ", string.Empty); ;// Read the input from the user.
                            DateTime time = DateTime.Now;
                            string dateToday = time.AddDays(1).ToString("yyyy'-'MM'-'dd"); // 2 days log
                            string dateBefore = time.AddDays(-5).ToString("yyyy'-'MM'-'dd");
                            ShowSignalsFromLogDB(numberData, dateToday, dateBefore);
                        }
                    }
                    catch (Exception)
                    {
                    }

                    //Application.Current.MainPage.Dispatcher.Dispatch(() => terminalSignalsList.Add(messege));
                    terminalSignalsList.Add(messege);
                    signals_terminal.ItemsSource = terminalSignalsList;

                    terminalSignalsCount++;
                    Application.Current.MainPage.Dispatcher.Dispatch(() => signals_terminal.ScrollTo(terminalSignalsCount));
                    
                }
            }
        }
        catch (Exception)
        {

        }
    }

原UI代码

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Black_Program.SignalsPage"
             Title="Сигнали">
    <ScrollView
        BackgroundColor="Black">
        <!-- Navigation Menu-->
        
        
        <!-- Test design -->
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="AUTO"/><!--0-->
                <RowDefinition Height="AUTO"/><!--1-->
                <RowDefinition Height="AUTO"/><!--2-->
                <RowDefinition Height="AUTO"/><!--3-->
                <RowDefinition Height="AUTO"/><!--4-->
                <RowDefinition Height="AUTO"/><!--5-->
                <RowDefinition Height="AUTO"/><!--6-->
                <RowDefinition Height="AUTO"/><!--7-->
            </Grid.RowDefinitions>


            <!-- Cool Adres and Users -->
            <HorizontalStackLayout
                HeightRequest="{OnPlatform Default='40',iOS='40',Android='35'}"
                Margin="{OnPlatform Default='5,0,10,0',iOS='5,0,5,0',Android='5,0,5,0'}"
                Grid.Row="1"
                HorizontalOptions="CenterAndExpand">
                
                <ScrollView>
                    <CollectionView
                        MaximumHeightRequest="300"
                        x:Name="signals_terminal"
                        SelectionMode="Single">
                        <CollectionView.ItemsSource>
                            <x:Array Type="{x:Type x:String}">
                                <x:String>Няма данни</x:String>
                            </x:Array>
                        </CollectionView.ItemsSource>
                    </CollectionView>
                </ScrollView>
            </Frame>
        </Grid>
    </ScrollView>
</ContentPage>

问题根源分析

  • UI线程操作违规:直接在后台线程修改数据源并设置ItemsSource,MAUI要求所有UI操作必须在主线程执行,否则会导致界面更新滞后或冻结。
  • 集合无变更通知:使用List<string>作为数据源,它不实现INotifyCollectionChanged接口,CollectionView无法自动感知集合变化,只有手动重置ItemsSource时才会更新,这就是点击后才显示缓存数据的原因。
  • 阻塞式IO操作:Thread.Sleep(10)和同步的stream.Read会阻塞后台线程,降低数据处理效率,拖慢UI更新节奏。
  • 编码转换错误:先通过ASCII拼接字符再用UTF8转换整个字节数组,会导致乱码或数据截断,且未使用读取到的length参数,会包含字节数组中的空字符。

修正方案

1. 使用可观察集合替换普通List

将List<string>改为ObservableCollection<string>,它会自动通知CollectionView集合变化,无需反复设置ItemsSource。

2. 所有UI操作切换到主线程

用Dispatcher.Dispatch包裹所有涉及UI元素的代码,包括集合添加、控件属性访问等。

3. 替换阻塞式操作为异步IO

使用await stream.ReadAsync替代同步stream.Read,去掉Thread.Sleep,避免阻塞线程。

4. 修复编码转换逻辑

仅转换实际读取到的字节数据,避免乱码。

修正后的代码

C#代码

using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.ObjectModel;
using Microsoft.Maui.Controls;

namespace Black_Program;

public partial class SignalsPage : ContentPage
{
    public static bool searchRedy = true;
    public static int daysNotPayedAlarm = 60;
    private readonly ObservableCollection<string> _terminalSignalsList = new();

    public SignalsPage()
    {
        InitializeComponent();
        signals_terminal.ItemsSource = _terminalSignalsList;
        _ = ReadFromIp();
    }

    async Task ReadFromIp()
    {
        while (true)
        {
            using var tcpclnt = new TcpClient();
            try
            {
                await tcpclnt.ConnectAsync(MainPage.sqlServerIP, MainPage.sqlServerTerminalPort);
                
                using var stream = tcpclnt.GetStream();
                var buffer = new byte[1024];
                
                while (tcpclnt.Connected)
                {
                    int length = await stream.ReadAsync(buffer, 0, buffer.Length);
                    if (length <= 0) break;

                    string message = Encoding.UTF8.GetString(buffer, 0, length);
                    
                    Dispatcher.Dispatch(() => 
                    {
                        _terminalSignalsList.Add(message);
                        signals_terminal.ScrollTo(_terminalSignalsList.Count - 1, position: ScrollToPosition.End);
                    });

                    await Dispatcher.DispatchAsync(async () =>
                    {
                        try
                        {
                            string searchText = signals_number_search.Text?.Trim() ?? string.Empty;
                            if (string.IsNullOrEmpty(searchText)) return;

                            string[] newMessages = message.Split('-', StringSplitOptions.RemoveEmptyEntries);
                            if (newMessages.Length > 0 && newMessages[0].TrimEnd() == searchText)
                            {
                                string numberData = searchText.Replace(" ", string.Empty);
                                DateTime now = DateTime.Now;
                                string dateToday = now.AddDays(1).ToString("yyyy-MM-dd");
                                string dateBefore = now.AddDays(-5).ToString("yyyy-MM-dd");
                                await ShowSignalsFromLogDB(numberData, dateToday, dateBefore);
                            }
                        }
                        catch { }
                    });
                }
            }
            catch
            {
                await Task.Delay(1000);
            }
        }
    }

    private Task ShowSignalsFromLogDB(string numberData, string dateToday, string dateBefore)
    {
        // 实现数据库查询逻辑
        return Task.CompletedTask;
    }
}

UI代码(修正闭合标签并移除硬编码数据源)

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Black_Program.SignalsPage"
             Title="Сигнали">
    <ScrollView BackgroundColor="Black">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="AUTO"/><!--0-->
                <RowDefinition Height="AUTO"/><!--1-->
                <RowDefinition Height="AUTO"/><!--2-->
                <RowDefinition Height="AUTO"/><!--3-->
                <RowDefinition Height="AUTO"/><!--4-->
                <RowDefinition Height="AUTO"/><!--5-->
                <RowDefinition Height="AUTO"/><!--6-->
                <RowDefinition Height="AUTO"/><!--7-->
            </Grid.RowDefinitions>

            <HorizontalStackLayout
                HeightRequest="{OnPlatform Default='40',iOS='40',Android='35'}"
                Margin="{OnPlatform Default='5,0,10,0',iOS='5,0,5,0',Android='5,0,5,0'}"
                Grid.Row="1"
                HorizontalOptions="CenterAndExpand">
                <ScrollView>
                    <CollectionView
                        MaximumHeightRequest="300"
                        x:Name="signals_terminal"
                        SelectionMode="Single">
                    </CollectionView>
                </ScrollView>
            </HorizontalStackLayout>
        </Grid>
    </ScrollView>
</ContentPage>

额外注意事项

  • 所有UI元素的访问和修改必须通过Dispatcher切换到主线程。
  • 使用using语句管理TcpClient和Stream的生命周期,避免资源泄漏。
  • 连接失败后添加延迟重试,防止频繁重试占用过高CPU。
  • 数据库操作需使用异步方法,避免阻塞线程。

内容的提问来源于stack exchange,提问作者Luki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 04:40:29