0

guys I am currently working on a existing WPF Application build with Prism build and .NET 4.6. I have added a new Page which contains a few User Controls. In one of the User Controls I need to load a csv with a few hundred rows(always under thousand) into a datagrid.

Every time I load the data into the datagrid the whole Application freezes for a few seconds. I have tried to do the loading async with the dispatcher but the application freezes while loading. I have also tried to do it with a task but then I always got an exception("only UI Thread is allowed to change the observable collection")

Below my current async implementation which did not work, any help or idea is highly appreciated.

Thank you

Product XAML

<UserControl x:Class="Module.UserControls.Products"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Module.UserControls"
             xmlns:mvvm="http://prismlibrary.com/"
             mvvm:ViewModelLocator.AutoWireViewModel="true"
             mc:Ignorable="d" 
             d:DesignHeight="650" d:DesignWidth="800" Background="{DynamicResource Module.Background}" MaxHeight="1500">
    <UserControl.Resources>
        <Style TargetType="DataGridCell">
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="FontSize" Value="16" />
            <Setter Property="Foreground" Value="{DynamicResource Module.Textbox.Foreground}" />
            <Setter Property="Margin" Value="5,5,5,5" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style TargetType="CheckBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="16" />
            <Setter Property="FontWeight" Value="Normal" />
            <Setter Property="Background" Value="{DynamicResource Module.Button.Background}" />
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
        </Style>
    </UserControl.Resources>
    <Grid>
        <Grid Margin="0,20,0,0" Background="{DynamicResource Module.Block.Background}">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Column="0" Grid.Row="0">
                <TextBlock Text="Products" Style="{DynamicResource Module.H2}" />
            </Label>
            <ScrollViewer Grid.Column="0" Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Margin="0,10,0,10" OverridesDefaultStyle="True" >
                <ScrollViewer.Resources>
                    <Style TargetType="{x:Type ScrollBar}">
                        <Setter Property="Background" Value="LightGray"/>
                    </Style>
                </ScrollViewer.Resources>
                <DockPanel  HorizontalAlignment="Stretch">
                    <DataGrid  AutoGenerateColumns = "False" IsReadOnly="False" ItemsSource="{Binding ProductCollection, UpdateSourceTrigger=PropertyChanged, IsAsync=True, Mode=TwoWay}" HorizontalAlignment="Center"  
                          Width="Auto" HorizontalContentAlignment="Center">
                        <DataGrid.Resources>
                            <Style  TargetType="DataGridCell">
                                <Setter Property="Foreground" Value="Black" />
                                <Setter Property="FontSize" Value="16" />
                                <Setter Property="BorderBrush" Value="White" />
                                <Style.Triggers>
                                    <Trigger Property="IsSelected" Value="True">
                                        <Setter Property="Background" Value="{x:Null}" />
                                        <Setter Property="BorderBrush" Value="{x:Null}" />
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </DataGrid.Resources>
                        <DataGrid.Columns>
                            <DataGridTextColumn Header = "Position" Binding="{Binding Path=InProductPos, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120" />
                            <DataGridTextColumn Header = "Layout Position" Binding="{Binding Path = InProductLayout, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Name" Binding="{Binding Path=InProductDescription, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="300"/>
                            <DataGridTextColumn Header = "Product Quantity" Binding="{Binding Path=InProductPieces, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Class" Binding="{Binding Path=InProductClass, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Product Part ID" Binding="{Binding Path=InProductPartID, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Number" Binding="{Binding Path=InProductNumber, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Part list Name" Binding="{Binding Path=InProductPartlistName, UpdateSourceTrigger=PropertyChanged}" Width="Auto" MinWidth="300"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </DockPanel>
            </ScrollViewer>
        </Grid>
    </Grid>
</UserControl>

Button XAML

<UserControl x:Class="Module.UserControls.ButtonRow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:resx="clr-namespace:Module.Properties"
             xmlns:local="clr-namespace:Module.UserControls"
             xmlns:mvvm="http://prismlibrary.com/"
             mvvm:ViewModelLocator.AutoWireViewModel="true"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />
        <Style TargetType="DataGridCell">
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="FontSize" Value="16" />
            <Setter Property="Foreground" Value="{DynamicResource Module.Textbox.Foreground}" />
            <Setter Property="Margin" Value="5,5,5,5" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style x:Key="StyleButtonWhite" TargetType="Button">
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style x:Key="StyleButtonWhiteWhite" TargetType="Button">
            <Setter Property="Foreground" Value="White" />
            <Setter Property="Background" Value="{x:Null}" />
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="16" />
            <Setter Property="FontWeight" Value="Normal" />
            <Setter Property="Background" Value="{DynamicResource Module.Button.Background}" />
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </UserControl.Resources>
    <Grid>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,10">
            <Button
                Margin="12,0,12,0"
                Command="{Binding LoadProductsCommand}"
                Width="101" Height="40"
                Style="{StaticResource StyleButtonWhite}"
                Background="{DynamicResource Module.Button.Background}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Import Products" Foreground="White" />
                </StackPanel>
            </Button>
        </StackPanel>
    </Grid>
</UserControl>

Part of the ViewModel

public DelegateCommand LoadProductsCommand => new DelegateCommand(LoadProducts, () => true);

private ObservableCollection<Product> _productCollection;

        public ObservableCollection<Products> ProductCollection
        {
            get => _productCollection;
            set
            {
                _productCollection = value;
                OnPropertyChanged();
            }
        }

        public async void LoadProducts() 
        {
            if (Dispatcher.CurrentDispatcher.CheckAccess())
            {
               await Dispatcher.CurrentDispatcher.InvokeAsync(() =>
               {
                   ProductCollection.AddRange(FileImport.ImportProducts());
               });
            }
        }

Class of the collection

        public string InProductPos
        {
            get => _inProductPos;
            set
            {
                _inProductPos = value;
                OnPropertyChanged();
            }
        }

        public string InProductLayout
        {
            get => _inProductLayout;
            set
            {
                _inProductLayout = value;
                OnPropertyChanged();
            }
        }

        public string InProductDescription
        {
            get => _inProductDescription;
            set
            {
                _inProductDescription = value;
                OnPropertyChanged();
            }
        }

        public int InProductPieces
        {
            get => _inProductPieces;
            set
            {
                _inProductPieces = value;
                OnPropertyChanged();
            }
        }

        public int InProductClass
        {
            get => _inProductClass;
            set
            {
                _inProductClass = value;
                OnPropertyChanged();
            }
        }

        public int InProductPartID
        {
            get => _inProductPartID;
            set
            {
                _inProductPartID = value;
                OnPropertyChanged();
            }
        }

        public string InProductNumber
        {
            get => _inProductNumber;
            set
            {
                _inProductNumber = value;
                OnPropertyChanged();
            }
        }

        public string InProductPartlistName
        {
            get => _inProductPartlistName;
            set
            {
                _inProductPartlistName = value;
                OnPropertyChanged();
            }
        }

kaylum
  • 13,833
  • 2
  • 22
  • 31
Manuel1234
  • 190
  • 8
  • 1
    `Dispatcher.InvokeAsync` won't help here, becaue `ProductCollection.AddRange` still runs in the UI thread. You should be using this: https://stackoverflow.com/a/14602121/1136211, https://stackoverflow.com/a/39977500/1136211 – Clemens Sep 22 '21 at 12:31
  • Or you could load the items in a worker thread, store them, and then only _add_ them in the ui-thread. Or you could asynchronously load them and add them one by one all in the ui-thread. – Haukinger Sep 22 '21 at 12:52
  • I have tried the solutions but the Application freeze when I try to load the items into the collection – Manuel1234 Sep 22 '21 at 13:30
  • Remove UpdateSourceTrigger=PropertyChanged and don't use it again until you understand what it does. – Andy Sep 22 '21 at 17:37
  • I would get the data on a different thread. Return it as a list of row viewmodels. Await that and back on the ui thread new up an observablecollection passing the list in the ctor. Then set productcollection to that. – Andy Sep 22 '21 at 17:42

1 Answers1

2

You may try something like shown below. It loads the Product collection in a background thread, and only adds them to the ObservableCollection in the UI thread.

public async Task LoadProducts() 
{
    var products = await Task.Run(() => FileImport.ImportProducts());
    
    ProductCollection.AddRange(products);
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • ImportsProducts method load the .csv file. The import of the csv is fast enough to did not freeze the app. But as you mentioned it seems that the loading into the observable collection freeze the app. – Manuel1234 Sep 22 '21 at 14:34
  • Then you should go with BindingOperations.EnableCollectionSynchronization. – Clemens Sep 22 '21 at 15:02
  • Or add Products in multiple chunks of a reasonable size with a short `await Task.Delay(...)` in between to let the UI keep up. – Clemens Sep 22 '21 at 15:17
  • A short task delay solved the UI Freeze while adding items with another thread in the Observable Collection. Thank you very much! – Manuel1234 Sep 23 '21 at 08:16