1

I'm looking on an advice on how to resolve a problem I've encountered while implementing a functionality similar to the "Wireshark" trace list view using WPF and MVVM (currently using the MVVM Light framework).

At the moment what I did was adding a ListView into my View, do the binding of "ItemsSource" to an ObservableCollection on the ViewModel and implement the "autoscroll" by adding an event handler on the "CollectionChanged" event of the ListView.Items collection.

This is the XAML of the ListView used on the View:

<ListView x:Name="listView" ItemsSource="{Binding TraceItems}">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Col1" Width="60" DisplayMemberBinding="{Binding Col1Data}" />
                <GridViewColumn Header="Col2" Width="60" DisplayMemberBinding="{Binding Col2Data}" />
                <GridViewColumn Header="Col3" Width="150" DisplayMemberBinding="{Binding Col3Data}" />
            </GridView>
        </ListView.View>
    </ListView>

This is the code behind of the View:

public partial class TraceView : UserControl
{
    public TraceView()
    {
        InitializeComponent();
        ((INotifyCollectionChanged)listView.Items).CollectionChanged += ListView_CollectionChanged;
    }

    private void ListView_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            listView.ScrollIntoView(listView.Items[listView.Items.Count - 1]);
        }
    }
}

The problem I've encountered with this solution is that performance is really bad. Every time an object is added in the observable collection the event handler is called, and so the "ScrollIntoView" method of the ListView. After a few minutes of continuous addition of elements, the UI stops responding and memory usage gets really high (an element is added into the collection about every 100 ms).

What could I do to solve the problem? Thank you.

UPDATE 1

Adding an example to reproduce the issue. With this example, on my computer the program gets stuck after around 10 minutes.

NOTE: For a typo, I wrote above that one item is added to the list every 100ms when I meant 10ms instead.

The ListView is used to visualize network communication trace so items are added very quickly. Maybe I could handle a kind of "queue" system where trace items are not inserted immediately in the list view but in a delayed manner? The problem with this solution is that the time gap between the real trace and the visualized one will always increase..

MainWindow.xaml

<Window x:Class="WpfListViewIssue.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="800" Width="1200">
    <Grid>
        <ListView x:Name="listView" ItemsSource="{Binding TraceItems}" >
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Col1" Width="80" DisplayMemberBinding="{Binding Col1Data}" />
                    <GridViewColumn Header="Col2" Width="80" DisplayMemberBinding="{Binding Col2Data}" />
                    <GridViewColumn Header="Col3" Width="80" DisplayMemberBinding="{Binding Col3Data}" />
                    <GridViewColumn Header="Col4" Width="1000" DisplayMemberBinding="{Binding Col4Data}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Collections.Specialized;
using System.Windows;
using GalaSoft.MvvmLight.Threading;

namespace WpfListViewIssue
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            DispatcherHelper.Initialize();
            InitializeComponent();
            ((INotifyCollectionChanged)listView.Items).CollectionChanged += ListView_CollectionChanged;
            DataContext = new MainViewModel();
        }

        private void ListView_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                listView.ScrollIntoView(listView.Items[listView.Items.Count - 1]);
            }
        }
    }
}

MainViewModel.cs

using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Threading;

namespace WpfListViewIssue
{
    public class TraceItem
    {
        public string Col1Data { get; set; }
        public string Col2Data { get; set; }
        public string Col3Data { get; set; }
        public string Col4Data { get; set; }
    }

    public class MainViewModel : ViewModelBase
    {
        private ObservableCollection<TraceItem> _traceItems;

        public ObservableCollection<TraceItem> TraceItems
        {
            get { return _traceItems; }
            set
            {
                if (value != _traceItems)
                {
                    _traceItems = value;
                    RaisePropertyChanged();
                }
            }
        }

        public MainViewModel()
        {
            TraceItems = new ObservableCollection<TraceItem>();
            AddItems();
        }

        private async void AddItems()
        {
            await Task.Factory.StartNew(() =>
                                        {
                                            var randBuffer = new byte[50];
                                            var random = new Random();
                                            for (int i = 0; i < Int32.MaxValue; ++i)
                                            {
                                                random.NextBytes(randBuffer);
                                                TraceItem item = new TraceItem()
                                                {
                                                    Col1Data = "Item1 N." + i,
                                                    Col2Data = "Item2 N." + i,
                                                    Col3Data = "Item3 N." + i,
                                                    Col4Data = "RandomData: " + BitConverter.ToString(randBuffer).Replace("-", "")
                                                };
                                                DispatcherHelper.CheckBeginInvokeOnUI(() => TraceItems.Add(item));
                                                Thread.Sleep(10);
                                            }
                                        });
        }
    }
}
titanicsnake
  • 75
  • 2
  • 9
  • Sounds like a virtualisation problem – TheGeneral Jun 26 '18 at 06:52
  • What controls do you have holding the listview? – TheGeneral Jun 26 '18 at 06:57
  • 1
    Also if you could create an extremely small minimal example that reproduces your problem you would find it would be much easier for us to help you – TheGeneral Jun 26 '18 at 07:07
  • @TheGeneral The View is a UserControl with a DockPanel, on top of the panel there is a ToolTrayBar with some buttons. The rest of the DockPanel is filled with the ListView. This UserControl is then used on the MainWindow View – titanicsnake Jun 26 '18 at 07:12
  • Make sure that you haven't disabled the UI virtualization and add elements less frequently. – mm8 Jun 26 '18 at 07:19
  • Does it work without scrolling faster? – Rekshino Jun 26 '18 at 07:32
  • @TheGeneral Updated the post with an example that reproduces the problem. – titanicsnake Jun 26 '18 at 09:48
  • @mm8 Virtualization isn't disabled (it should be enabled on the ListView by default right ?). See the update on the possibile solution to add elements less frequently. – titanicsnake Jun 26 '18 at 09:49
  • @Rekshino Scrolling is done by the "ScrollIntoView" in the event handler. If the autoscroll is removed the listview has no issues at all. Autoscroll is a required feature though :( – titanicsnake Jun 26 '18 at 09:49
  • 2
    Why are you adding a new item every 10ms? This makes no sense. You need to throttle the updates to the UI. No user will be able to react to changes that happens every 10ms anyway. Also, storing references to Int32.MaxValue number of items in a collection is a bad idea. – mm8 Jun 26 '18 at 09:50
  • @mm8 You are right ! I'm now thinking of a way to implement this throttling. Maybe I can put the elements to be added in a "buffer" and every few seconds insert the contents of the buffer in the list view. Int32.MaxValue was only used in the example – titanicsnake Jun 26 '18 at 11:12
  • 10 ms is too fast! With 100ms your example works good by me. It will also work with 10 ms if you do Invoke instead of BeginInvoke, but it can be slowlier. See also [Dispatcher Invoke(…) vs BeginInvoke(…) confusion](https://stackoverflow.com/questions/19009174/dispatcher-invoke-vs-begininvoke-confusion) – Rekshino Jun 26 '18 at 11:19
  • You may implement your "buffer" or take a look at Reactive Extensions (Rx): https://github.com/dotnet/reactive – mm8 Jun 26 '18 at 11:30

0 Answers0