Reputation: 85
I'm struggling to find a solution with a performance problem on WPF. I have a C# project that receives some "messages" from a serial port and displays them on a GUI. This GUI allow the editing of the bytes of each received message and also have a "save" button that saves the edited messages into a file.
For the GUI, using MVVM with WPF, what I did to implement the above requirement is the following:
The View is showing the collection of "SerialMessageViewModel" using a ListView and for each SerialMessage I'm using an ItemsControl to show the MessageData bytes. Since I want thoose bytes to be editable I'm using a TextBox as ItemTemplate of the ItemsControl. The idea here is to have the ItemsControl to work as an "HexEditor" like GUI.
Everything is actually working fine except for the scrolling of the ListView which is very slow.
This makes the application very frustrating to use..
I understand that using a ListView of ItemsControl with TextBoxes like this does generate a lot of TextBoxes, but I can't find another way of making something similar.
The Visual Studio profiling tool shows that the lag in the scrolling is caused by "layout" time.
Any suggestions on how to solve this problem? Thank you very much !
View XAML
<Window x:Class="WpfListIssue.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"
xmlns:local="clr-namespace:WpfListIssue"
mc:Ignorable="d"
Title="MainWindow" Height="800" Width="600">
<Window.Resources>
<ResourceDictionary>
<local:ByteToHexStringConverter x:Key="ByteToHexStringConverter" />
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ProgressBar Grid.Row="0" Height="20" IsIndeterminate="True" />
<ListView Grid.Row="1" Margin="10" ItemsSource="{Binding Messages}">
<ListView.View>
<GridView>
<GridViewColumn Header="Message Name" Width="120" DisplayMemberBinding="{Binding MessageName}" />
<GridViewColumn Header="Message Data" Width="400" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding MessageData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Value, Converter={StaticResource ByteToHexStringConverter}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
ViewModels
public class MainViewModel : ViewModelBase
{
private ObservableCollection<SerialMessageViewModel> _messages;
public ObservableCollection<SerialMessageViewModel> Messages
{
get { return _messages; }
set
{
if (Equals(value, _messages)) return;
_messages = value;
RaisePropertyChanged();
}
}
public MainViewModel()
{
GetMessages();
}
private void GetMessages()
{
// Simulates the reception of messages from serial
Messages = new ObservableCollection<SerialMessageViewModel>();
Random rand = new Random();
for (int i = 0; i < 100; i++)
{
byte[] data = new byte[50];
rand.NextBytes(data);
SerialMessageViewModel message = new SerialMessageViewModel("Message n." + i, data);
Messages.Add(message);
}
}
}
public class SerialMessageViewModel : ViewModelBase
{
private string _messageName;
private ObservableCollection<NotifyByte> _messageData;
public string MessageName
{
get { return _messageName; }
set
{
if (Equals(value, _messageData)) return;
_messageName = value;
RaisePropertyChanged();
}
}
public ObservableCollection<NotifyByte> MessageData
{
get { return _messageData; }
set
{
if (Equals(value, _messageData)) return;
_messageData = value;
RaisePropertyChanged();
}
}
public SerialMessageViewModel(string messageName, byte[] data)
{
MessageName = messageName;
MessageData = new ObservableCollection<NotifyByte>();
foreach (byte b in data)
MessageData.Add(new NotifyByte(b));
}
}
public class NotifyByte : ViewModelBase
{
private byte _value;
public byte Value
{
get { return _value; }
set
{
if (Equals(value, _value)) return;
_value = value;
RaisePropertyChanged();
}
}
public NotifyByte(byte value)
{
_value = value;
}
}
ByteToHexStringConverter
public class ByteToHexStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
byte num = (byte)value;
return num.ToString("X2");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string input = (string)value;
if (!String.IsNullOrEmpty(input))
{
input = input.Replace(" ", "");
return System.Convert.ToByte(input, 16);
}
return 0x00;
}
}
Upvotes: 1
Views: 781
Reputation: 792
This doesn't use your example code, but in general here's how you could use a DataGrid to implement what you're looking for, but to try and make it more lightweight. Note that I'm using a TextBlock since we don't want to allow editing from the cell. Your custom text box XAML would go in the CellEditTemplate.
MainWindow.xaml
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding DataSet}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding DisplayName}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayForIsSomethingRelevant}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSomethingRelevant}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
MainWindowVm.cs
public class MainWindowVm
{
public ObservableCollection<TestObject> DataSet { get; set; } = new ObservableCollection<TestObject>();
public MainWindowVm()
{
DataSet.Add(new TestObject {DisplayName = "Booyah", IsSomethingRelevant = true});
DataSet.Add(new TestObject {DisplayName = "Iggy Boop", IsSomethingRelevant = false});
}
}
TestObject.cs (bindablebase just provides default implementations for notifypropertychanged)
public class TestObject : BindableBase
{
private string _displayName;
public string DisplayName
{
get { return _displayName; }
set { SetProperty(ref _displayName, value); }
}
private bool _IsSomethingRelevant;
public bool IsSomethingRelevant
{
get { return _IsSomethingRelevant; }
set
{
SetProperty(ref _IsSomethingRelevant, value);
NotifyPropertyChanged(nameof(DisplayForIsSomethingRelevant));
}
}
public string DisplayForIsSomethingRelevant => IsSomethingRelevant
? "Totes Relevant"
: "Non-Relevono";
}
Upvotes: 1