DataGrid generates empty rows - c#

I have two threads - let's name them Calc thread and UI thread. Inside the Calc thread I refreshes an ObservableCollection. I also have a handler for the CollectionCHanged event of the ObservableCollection. As I know, the handler executes within the same thread that raises the CollectionChanged event - so that is the same thread that refreshes the ObservableCollection in my case. So, to refresh UI I can't use bindings directly as in single-threaded application - UI must be refreshed manually through Dispatcher. But when I use DataGrid in the UI I get the empty rows instead of any data, and when I use ListBox, for example, the appropriate data is showed:
data grid case to the left, list box case to the right
(list box is just for example that the data binds and shows; I don't want the data to be showed like in this list box, but like in data grid (if it worked as I expect - not as in case on the picture) - table with column titles)
Well, I prepared the code, which you can copy and paste to reconstruct the problem:
C#
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Threading;
using System.Windows;
namespace WpfApplication1
{
public class MyClass
{
public int Integer { get; set; }
public string Str { get; set; }
}
public partial class MainWindow : Window
{
public ObservableCollection<MyClass> MyCollection { get; set; }
public MainWindow()
{
InitializeComponent();
MyCollection = new ObservableCollection<MyClass>();
MyCollection.CollectionChanged += MyCollection_CollectionChanged;
Thread t = new Thread(new ThreadStart(() =>
{
for (int i = 0; i < 10; i++)
{
MyCollection.Add(new MyClass()
{
Integer = i,
Str = "String" + i
});
Thread.Sleep(500);
}
}));
t.Start();
}
void MyCollection_CollectionChanged(
object sender,
NotifyCollectionChangedEventArgs e)
{
Dispatcher.Invoke(
() =>
{
foreach (var item in e.NewItems)
dataGrid.Items.Add((MyClass)item);
});
}
}
}
XAML (just comment/uncomment the list box case and the data grid case):
<Window
x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<!--<ListBox Name="dataGrid">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Integer}" />
<TextBlock Text="{Binding Path=Str}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>-->
<DataGrid Name="dataGrid">
<DataGrid.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Integer}" />
<TextBlock Text="{Binding Path=Str}" />
</StackPanel>
</DataTemplate>
</DataGrid.ItemTemplate>
</DataGrid>
</Grid>
</Window>

Is this what you want?
C#
namespace WpfApplication1
{
public class MyClass
{
public int Integer { get; set; }
public string Str { get; set; }
}
public partial class MainWindow : Window
{
public ObservableCollection<MyClass> MyCollection { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyCollection = new ObservableCollection<MyClass>();
Thread t = new Thread(new ThreadStart(() =>
{
for (int i = 0; i < 10; i++)
{
Dispatcher.Invoke(new Action(() =>
{
MyCollection.Add(new MyClass()
{
Integer = i,
Str = "String " + i
});
}));
}
}));
t.Start();
}
}
}
XAML
<Window
x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Name="dataGrid" ItemsSource="{Binding MyCollection}">
<DataGrid.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Integer}" />
<TextBlock Text="{Binding Str}" />
</StackPanel>
</DataTemplate>
</DataGrid.ItemTemplate>
</DataGrid>
</Grid>
</Window>
Other method is to use another List:
public partial class MainWindow : Window
{
private List<MyClass> _MyCollection;
public ObservableCollection<MyClass> MyCollection { get; set; }
private DispatcherTimer dispatcherTimer = new DispatcherTimer();
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyCollection = new ObservableCollection<MyClass>();
_MyCollection = new List<MyClass>();
dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 500);
dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
Thread t = new Thread(new ThreadStart(() =>
{
for (int i = 0; i < 10; i++)
{
_MyCollection.Add(new MyClass()
{
Integer = i,
Str = "String " + i
});
Thread.Sleep(500);
}
}));
t.Start();
dispatcherTimer.Start();
}
private void dispatcherTimer_Tick(object sender, EventArgs e)
{
if (_MyCollection.Count != MyCollection.Count)
{
MyCollection.Add(_MyCollection[_MyCollection.Count - 1]);
}
}
}
Second edit with you example:
namespace WpfApplication1
{
public class MyClass
{
public int Integer { get; set; }
public string Str { get; set; }
}
public partial class MainWindow : Window
{
private ObservableCollection<MyClass> _MyCollection;
public ObservableCollection<MyClass> MyCollection { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyCollection = new ObservableCollection<MyClass>();
_MyCollection = new ObservableCollection<MyClass>();
_MyCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(_MyCollection_CollectionChanged);
Thread t = new Thread(new ThreadStart(() =>
{
for (int i = 0; i < 10; i++)
{
_MyCollection.Add(new MyClass()
{
Integer = i,
Str = "String " + i
});
Thread.Sleep(500);
}
}));
t.Start();
}
private void _MyCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Dispatcher.Invoke(new Action(
() =>
{
foreach (var item in e.NewItems)
MyCollection.Add((MyClass)item);
}));
}
}
}

Related

Cannot get selected ComboBox items in MainWindow.xaml.cs

I'd like to access ComboBox items (which are defined in another class) in MainWindow.xaml.cs, but I can't.
I'm new to C# and WPF. The purpose of this code is to get a selected ComboBox item as a search key. I have copied from many example codes on the Internet and now I'm completely lost. I don't even know which part is wrong. So, let me show the entire codes (sorry):
MainWindow.xaml:
<Window x:Class="XY.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:XY"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="454.4">
<Grid>
<DataGrid ItemsSource="{Binding channels}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Margin="0,0,0,-0.2">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding name}"
Header="Channel" Width="Auto"/>
<DataGridTemplateColumn Width="100" Header="Point Setting">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="piontsComboBox"
ItemsSource="{Binding DataContext.points,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
SelectionChanged="PrintText"
DisplayMemberPath="name"
SelectedValuePath="name"
Margin="5"
SelectedItem="{Binding DataContext.SelectedPoint,
RelativeSource={RelativeSource AncestorType={x:Type Window}},
Mode=TwoWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TextBox x:Name="tb" Width="140" Height="30" Margin="10,250,200,30"></TextBox>
<Button x:Name="Browse_Button" Content="Browse" Margin="169,255,129.6,0"
Width="75" Click="Browse_Button_Click" Height="30" VerticalAlignment="Top"/>
</Grid>
MainWindow.xaml.cs:
using System;
using System.Windows;
using System.Windows.Controls;
namespace XY
{
public partial class MainWindow : Window
{
public GridModel gridModel { get; set; }
public MainWindow()
{
InitializeComponent();
gridModel = new GridModel();
this.DataContext = gridModel;
}
private void Browse_Button_Click(object sender, RoutedEventArgs e)
{
WakeupClass clsWakeup = new WakeupClass();
clsWakeup.BrowseFile += new EventHandler(gridModel.ExcelFileOpen);
clsWakeup.Start();
}
void PrintText(object sender, SelectionChangedEventArgs args)
{
//var item = pointsComboBox SelectedItem as Point;
//if(item != null)
//{
// tb.Text = "You selected " + item.name + ".";
//}
MessageBox.Show("I'd like to show the item.name in the TextBox.");
}
}
public class WakeupClass
{
public event EventHandler BrowseFile;
public void Start()
{
BrowseFile(this, EventArgs.Empty);
}
}
}
Point.cs:
namespace XY
{
public class Point : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
OnPropertyChanged("code");
}
}
}
}
Record.cs:
namespace XY
{
public class Record : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _PointCode;
public int PointCode
{
get { return _PointCode; }
set
{
_PointCode = value;
OnPropertyChanged("PointCode");
}
}
private Record _selectedRow;
public Record selectedRow
{
get { return _selectedRow; }
set
{
_selectedRow = value;
OnPropertyChanged("SelectedRow");
}
}
private Point _selectedPoint;
public Point SelectedPoint
{
get { return _selectedPoint; }
set
{
_selectedPoint = value;
_selectedRow.PointCode = _selectedPoint.code;
OnPropertyChanged("SelectedRow");
}
}
}
}
ViewModelBase.cs:
using System.ComponentModel;
namespace XY
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
}
GridModel.cs:
using System.Collections.ObjectModel;
using System.Windows;
namespace XY
{
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> channels { get; set; }
public ObservableCollection<Point> points { get; set; }
public GridModel()
{
channels = new ObservableCollection<Record>() {
new Record {name = "High"},
new Record {name = "Middle"},
new Record {name = "Low"}
};
}
internal void ExcelFileOpen(object sender, System.EventArgs e)
{
points = new ObservableCollection<Point> { new Point { } };
points.Add(new Point { name = "point1", code = 1 });
points.Add(new Point { name = "point2", code = 2 });
points.Add(new Point { name = "point3", code = 3 });
MessageBox.Show("Assume that Excel data are loaded here.");
}
}
}
The procedure goes like:
Click on the "Browse" button to load the data.
Click on the 1st column "Channel" to sort the list (This is a bug, but if you don't, the "Point Setting" items won't show up).
Click on the "Point Setting" ComboBox to select the items (point1, point2, ..., etc.).
This code is supposed to show the selected item name in the TextBox.
If everything is in MainWindow.xaml.cs, the ComboBox items could be accessed. Since I split the codes into different classes, it has not been working. Please help me. Any suggestion would be helpful.
Your binding does work. You can make use of the sender object to achieve what you wanted.
void PrintText(object sender, SelectionChangedEventArgs args)
{
var comboBox = sender as ComboBox;
var selectedPoint = comboBox.SelectedItem as Point;
tb.Text = selectedPoint.name;
}
The problem is that the DataGridColumn is not part of the WPF logical tree and so your relative source binding will not work. The only way to get your binding to work is a type of kluge (very common with WPF in my experience). Create a dummy element that is in the logical tree and then reference that.
So
<FrameworkElement x:Name="dummyElement" Visibility="Collapsed"/>
<DataGrid ItemsSource="{Binding channels}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Margin="0,0,0,-0.2">
Then your binding will look like this
<ComboBox x:Name="piontsComboBox"
ItemsSource="{Binding DataContext.points,
Source={x:Reference dummyElement}}"
SelectionChanged="PrintText"
DisplayMemberPath="name"
SelectedValuePath="name"
Margin="5"
SelectedItem="{Binding DataContext.SelectedPoint,
Source={x:Reference dummyElement},
Mode=TwoWay}"/>

ComboBox items don't show up until the 1st column is sorted

The 2nd column items "Point Setting" don't show up until the 1st column items are sorted, clicking on the header of the 1st column. The goal of this code is to link the 1st and 2nd column items, then use the 2nd column items as the search keys.
I'm new to C# and WPF.
I tired to put sequential numbers in front of the 1st column items (1., 2., and so on) because I thought it would solve the problem if those items are initially sorted. But, no luck. I heard that ObservableCollection<> doesn't manage the input order, so once I changed it with List<>. But it didn't solve this problem, too.
Actually, I don't want to sort the 1st column; they should be fixed and no need to change the order/number at all.
To avoid any confusions, let me show my entire codes (sorry).
MainWindow.xaml:
<Window x:Class="XY.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:XY"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="454.4">
<Grid>
<DataGrid ItemsSource="{Binding channels}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
Margin="0,0,0,-0.2">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding name}"
Header="Channel" Width="Auto"/>
<DataGridTemplateColumn Width="100" Header="Point Setting">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="piontsComboBox"
ItemsSource="{Binding DataContext.points,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
SelectionChanged="PrintText"
DisplayMemberPath="name"
SelectedValuePath="name"
Margin="5"
SelectedItem="{Binding DataContext.SelectedPoint,
RelativeSource={RelativeSource AncestorType={x:Type Window}},
Mode=TwoWay}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TextBox x:Name="tb" Width="140" Height="30" Margin="10,250,200,30"></TextBox>
<Button x:Name="Browse_Button" Content="Browse" Margin="169,255,129.6,0"
Width="75" Click="Browse_Button_Click" Height="30" VerticalAlignment="Top"/>
</Grid>
</Window>
MainWindow.xaml.cs:
using System;
using System.Windows;
using System.Windows.Controls;
namespace XY
{
public partial class MainWindow : Window
{
public GridModel gridModel { get; set; }
public MainWindow()
{
InitializeComponent();
gridModel = new GridModel();
this.DataContext = gridModel;
}
private void Browse_Button_Click(object sender, RoutedEventArgs e)
{
WakeupClass clsWakeup = new WakeupClass();
clsWakeup.BrowseFile += new EventHandler(gridModel.ExcelFileOpen);
clsWakeup.Start();
}
void PrintText(object sender, SelectionChangedEventArgs args)
{
var comboBox = sender as ComboBox;
var selectedPoint = comboBox.SelectedItem as Point;
tb.Text = selectedPoint.name;
}
}
public class WakeupClass
{
public event EventHandler BrowseFile;
public void Start()
{
BrowseFile(this, EventArgs.Empty);
}
}
}
ViewModelBase.cs:
using System.ComponentModel;
namespace XY
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
}
Point.cs:
namespace XY
{
public class Point : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _code;
public int code
{
get { return _code; }
set
{
_code = value;
OnPropertyChanged("code");
}
}
}
}
Record.cs:
namespace XY
{
public class Record : ViewModelBase
{
private string _name;
public string name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("name");
}
}
private int _PointCode;
public int PointCode
{
get { return _PointCode; }
set
{
_PointCode = value;
OnPropertyChanged("PointCode");
}
}
private Record _selectedRow;
public Record selectedRow
{
get { return _selectedRow; }
set
{
_selectedRow = value;
OnPropertyChanged("SelectedRow");
}
}
private Point _selectedPoint;
public Point SelectedPoint
{
get { return _selectedPoint; }
set
{
_selectedPoint = value;
_selectedRow.PointCode = _selectedPoint.code;
OnPropertyChanged("SelectedRow");
}
}
}
}
GridModel.cs:
using System.Collections.ObjectModel;
using System.Windows;
namespace XY
{
public class GridModel : ViewModelBase
{
public ObservableCollection<Record> channels { get; set; }
public ObservableCollection<Point> points { get; set; }
public GridModel()
{
channels = new ObservableCollection<Record>() {
new Record {name = "1. High"},
new Record {name = "2. Middle"},
new Record {name = "3. Low"}
};
}
internal void ExcelFileOpen(object sender, System.EventArgs e)
{
points = new ObservableCollection<Point> { new Point { } };
MessageBox.Show("Please assume that Excel data are loaded here.");
points.Add(new Point { name = "point1", code = 1 });
points.Add(new Point { name = "point2", code = 2 });
points.Add(new Point { name = "point3", code = 3 });
points.Add(new Point { name = "point4", code = 4 });
}
}
}
The procedure goes like:
Click on the "Browse" button to load the data.
Click on the 1st column "Channel" to sort the list (GOAL: I'd like to GET RID OF this step).
Click on the "Point Setting" ComboBox to select the items (point1, point2, ..., etc.).
... I don't know if ObservableCollection<> is appropriate here. If List<> or any other type is better, please change it. Any suggestion would be helpful. Thank you in advance.
Change your points ObservableCollection like such, because you're setting the reference of the collection after the UI is rendered, you would need to trigger the PropertyChanged event to update the UI.
private ObservableCollection<Point> _points;
public ObservableCollection<Point> points
{
get { return _points; }
set
{
_points = value;
OnPropertyChanged(nameof(points));
}
}
An alternative would be to first initialise your collection.
public ObservableCollection<Point> points { get; set; } = new ObservableCollection<Point>();
internal void ExcelFileOpen(object sender, System.EventArgs e)
{
// Do not re-initialise the collection anymore.
//points = new ObservableCollection<Point> { new Point { } };
points.Add(new Point { name = "point1", code = 1 });
points.Add(new Point { name = "point2", code = 2 });
points.Add(new Point { name = "point3", code = 3 });
}

How to dispose Image in WPF

I create a list and set it's datatemplete as Image.
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="411" Margin="45,24,0,0" VerticalAlignment="Top" Width="336">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding ImgSource}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
then I load 100 pictures
public partial class MainWindow : Window
{
List<test> lstTest = new List<test>();
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 100; i++)
{
test t = new test();
t.ImgSource = #"d:\test\1.jpg";
lstTest.Add(t);
}
}
private void btnHead_Click(object sender, RoutedEventArgs e)
{
listBox.ItemsSource = lstTest;
}
private void btnClear_Click(object sender, RoutedEventArgs e)
{
lstTest = null;
listBox.ItemsSource = null;
}
public class test
{
public string ImgSource { get; set; }
}
}
before I click btnHead,the mem is 20m, and when I click this button, the mem increase to 40m.
I want to create a function to release mem (return 20m), but btnClear_Click do not work.
How to dispose Image in wpf?

Timer to Update DataContext in MVVM-WPF

I have a text log File which i parse every 10 seconds to display it values on the WPF-Application, I am trying to use MVVM for the first time in WPF. The problem i am facing is I am unable to refresh the DataContext with the timer.
The format of the text file is
log.txt
UserID|RP1|MS9|1.25
UserID|RP5|MS7|1.03
Code for the Application is given below
Code for Model-Class
public class UserModel
{
public string userID{get; set;}
public string RP{get; set;}
public string MS{get; set;}
public string Rate{get; set;}
}
Code for ModelView-Class
public class AppModelView
{
private ObservableCollection<UserModel> _userList;
DispatcherTimer LogTimer;
public AppModelView()
{
_userList = new ObservableCollection<UserModel>();
LogTimer = new DispatcherTimer();
LogTimer.Interval = TimeSpan.FromMilliseconds(10000);
LogTimer.Tick += (s, e) =>
{
foreach(DataRow row in LogManager.Record) //LogManager is class which parse the txt file and assign value into a DataTable Record
_userList.add(new UserModel
{
userID= row[0].toString();
RP = row[1].toString();
MS = row[2].toString();
rate = row[3].toString();
});
};
LogTimer.Start();
}
public ObservableCollection<UserModel> UserList
{
get { return _userList; }
set { _userList = value;
NotifyPropertyChanged("UserList");}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
MainWindows.xaml
<Window x:Class="MonitoringSystem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Monitoring Server" WindowStartupLocation="CenterScreen" Height="768" WindowState="Maximized" >
<Grid>
<DockPanel>
<Label Content="User Monitored" DockPanel.Dock="Top"/>
<ListView Name="lstRpt" DockPanel.Dock="Bottom" ItemsSource="{Binding UserList}" >
<ListView.View>
<GridView>
<GridViewColumn Header="UserID" DisplayMemberBinding="{Binding userID}"/>
<GridViewColumn Header="RP" DisplayMemberBinding="{Binding RP}"/>
<GridViewColumn Header="MS" DisplayMemberBinding="{Binding MS}"/>
<GridViewColumn Header="Rate" DisplayMemberBinding="{Binding Rate}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Grid>
</Windows>
MainWindows.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AppViewModel VM = new AppViewModel();
this.DataContext = VM;
}
}
Now if I remove the DispatcherTimer it display the values which were parse for the first time and display it , but with timer it cannot display any values.
Your Guidance is highly appreciated.
What I suspect is happening is that your UserModel is getting added to the collection before its properties have been set, and because your UserModel has no INPC the view never updates once they are set.
Try changing your code to:
LogTimer.Tick += (s, e) =>
{
foreach(DataRow row in LogManager.Record) //LogManager is class which parse the txt file and assign value into a DataTable Record
{
var userModel = new UserModel
{
userID= row[0].toString();
RP = row[1].toString();
MS = row[2].toString();
rate = row[3].toString();
};
_userList.Add(userModel);
};
LogTimer.Start();
};
Correction needed only in ViewModel
public class AppModelView
{
private ObservableCollection<UserModel> _userList;
DispatcherTimer LogTimer;
public AppModelView()
{
_userList = new ObservableCollection<UserModel>();
LogTimer = new DispatcherTimer();
LogTimer.Interval = TimeSpan.FromMilliseconds(10000);
LogTimer.Tick += (s, e) =>
{
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal,
new Action(
delegate()
{
UserList.Add(new UserModel
{
userID = "test"
});
}
)
);
};
LogTimer.Start();
}
public ObservableCollection<UserModel> UserList
{
get { return _userList; }
set
{
_userList = value;
NotifyPropertyChanged("UserList");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}

Why does CollectionViewSource.GetDefaultView(...) return the wrong CurrentItem from inside a Task thread?

I have what I think is a fairly standard setup, a ListBox backed by an ObservableCollection.
I have some work to do with the Things in the ObservableCollection which might take a significant amount of time (more than a few hundred milliseconds) so I'd like to offload that onto a Task (I could have also used BackgroundWorker here) so as to not freeze the UI.
What's strange is that when I do CollectionViewSource.GetDefaultView(vm.Things).CurrentItem before starting the Task, everything works as expected, however if this happens during the Task then CurrentItem seems to always point to the first element in the ObservableCollection.
I've drawn up a complete working example.
XAML:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<ToolBar DockPanel.Dock="Top">
<Button Content="Click Me Sync" Click="ButtonSync_Click" />
<Button Content="Click Me Async Good" Click="ButtonAsyncGood_Click" />
<Button Content="Click Me Async Bad" Click="ButtonAsyncBad_Click" />
</ToolBar>
<TextBlock DockPanel.Dock="Bottom" Text="{Binding Path=SelectedThing.Name}" />
<ListBox Name="listBox1" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=Things}" SelectedItem="{Binding Path=SelectedThing}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
C#:
public partial class MainWindow : Window
{
private readonly ViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new ViewModel();
DataContext = vm;
}
private ICollectionView GetCollectionView()
{
return CollectionViewSource.GetDefaultView(vm.Things);
}
private Thing GetSelected()
{
var view = GetCollectionView();
return view == null ? null : (Thing)view.CurrentItem;
}
private void NewTask(Action start, Action finish)
{
Task.Factory
.StartNew(start)
.ContinueWith(t => finish());
//.ContinueWith(t => finish(), TaskScheduler.Current);
//.ContinueWith(t => finish(), TaskScheduler.Default);
//.ContinueWith(t => finish(), TaskScheduler.FromCurrentSynchronizationContext());
}
private void ButtonSync_Click(object sender, RoutedEventArgs e)
{
var thing = GetSelected();
DoWork(thing);
MessageBox.Show("all done");
}
private void ButtonAsyncGood_Click(object sender, RoutedEventArgs e)
{
var thing = GetSelected(); // outside new task
NewTask(() =>
{
DoWork(thing);
}, () =>
{
MessageBox.Show("all done");
});
}
private void ButtonAsyncBad_Click(object sender, RoutedEventArgs e)
{
NewTask(() =>
{
var thing = GetSelected(); // inside new task
DoWork(thing); // thing will ALWAYS be the first element -- why?
}, () =>
{
MessageBox.Show("all done");
});
}
private void DoWork(Thing thing)
{
Thread.Sleep(1000);
var msg = thing == null ? "nothing selected" : thing.Name;
MessageBox.Show(msg);
}
}
public class ViewModel
{
public ObservableCollection<Thing> Things { get; set; }
public Thing SelectedThing { get; set; }
public ViewModel()
{
Things = new ObservableCollection<Thing>();
Things.Add(new Thing() { Name = "one" });
Things.Add(new Thing() { Name = "two" });
Things.Add(new Thing() { Name = "three" });
Things.Add(new Thing() { Name = "four" });
}
}
public class Thing
{
public string Name { get; set; }
}
I believe CollectionViewSource.GetDefaultView is effectively thread-static - in other words, each thread will see a different view. Here's a short test to show that:
using System;
using System.Windows.Data;
using System.Threading.Tasks;
internal class Test
{
static void Main()
{
var source = "test";
var view1 = CollectionViewSource.GetDefaultView(source);
var view2 = CollectionViewSource.GetDefaultView(source);
var view3 = Task.Factory.StartNew
(() => CollectionViewSource.GetDefaultView(source))
.Result;
Console.WriteLine(ReferenceEquals(view1, view2)); // True
Console.WriteLine(ReferenceEquals(view1, view3)); // False
}
}
If you want your task to work on a particular item, I suggest you fetch that item before starting the task.

Categories