I am making a wpf application that has 2 datagrids and I want them to scroll together.
I know that the DataGridView class has a scroll event that you can use make the necessary changes to the other grid, but DataGrids don't have a Scroll event. I MUST use a DataGrid.
This example is good but is not WPF and it's using DataGridView instead of DataGrid. Using one scroll bar to control two DataGridView
What is the best way to have it so that one data grid's scroll bar also will move the data grid's scroll bar in WPF and DataGrids?
You can do this by getting the underlying ScrollViewer of both DataGrid's and setting up the event accordingly. Below is a quick example I made that appears to work as you've described.
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" Loaded="Window_Loaded">
<Grid>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Margin="52,69,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="200" ItemsSource="{Binding Collection}" />
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Margin="270,69,0,0" Name="dataGrid2" VerticalAlignment="Top" Width="200" ItemsSource="{Binding Collection}" />
</Grid>
</Window>
code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ObservableCollection<Person> _collection = new ObservableCollection<Person>();
ScrollViewer scrollView = null;
ScrollViewer scrollView2 = null;
public MainWindow()
{
for (int i = 0; i < 50; i++)
{
var p = new Person() { Name = string.Format("{0}", i), Age = i };
_collection.Add(p);
}
this.DataContext = this;
InitializeComponent();
}
void scrollView_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var newOffset = e.VerticalOffset;
if ((null != scrollView) && (null != scrollView2))
{
scrollView.ScrollToVerticalOffset(newOffset);
scrollView2.ScrollToVerticalOffset(newOffset);
}
}
public ObservableCollection<Person> Collection
{
get
{
return _collection;
}
}
private ScrollViewer getScrollbar(DependencyObject dep)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dep); i++)
{
var child = VisualTreeHelper.GetChild(dep, i);
if ((null != child) && child is ScrollViewer)
{
return (ScrollViewer)child;
}
else
{
ScrollViewer sub = getScrollbar(child);
if (sub != null)
{
return sub;
}
}
}
return null;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
scrollView = getScrollbar(dataGrid1);
scrollView2 = getScrollbar(dataGrid2);
if (null != scrollView)
{
scrollView.ScrollChanged += new ScrollChangedEventHandler(scrollView_ScrollChanged);
}
if (null != scrollView2)
{
scrollView2.ScrollChanged += new ScrollChangedEventHandler(scrollView_ScrollChanged);
}
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
What is happening is that I'm iterating through the VisualTree of both DataGrids upon Window load using getScrollbar. I then set up the scroll changed event for both DataGrids and then scroll to the vertical offset that was just changed inside the scroll changed event handler.
Related
Im quite new to coding. So far I have a WPF application that when I press submit it creates the treeview but I wanted to add a countdown timer for each child item and have it display the time remaining next to the child item. The problem is the treeview doesn't update and I dont know how to assign a timer for each child item
using Microsoft.Azure.Cosmos.Core.Collections;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace Test_v2
{
public partial class MainWindow : Window
{
public int secondsCount = 100;
public MainWindow()
{
InitializeComponent();
DispatcherTimer disTmr = new DispatcherTimer();
disTmr.Tick += new EventHandler(disTmr_Tick);
disTmr.Interval = new TimeSpan(0, 0, 1);
disTmr.Start();
}
public void disTmr_Tick(object sender, EventArgs e)
{
secondsCount--;
}
List<TreeViewItem> folderList = new List<TreeViewItem>();
public void SubmitButton_Click(object sender, RoutedEventArgs e)
{
if (Folder.Text.Length == 0)
{
ErrorBlock.Text = "Please Enter Folder Name";
return;
}
if (Name.Text.Length == 0)
{
ErrorBlock.Text = "Please Enter a Name";
return;
}
TreeViewItem parent = new TreeViewItem();
for (int i = 0; i < folderList.Count; i++)
{
if (folderList[i].Header.ToString() == Folder.Text)
{
parent = folderList[i];
break;
}
}
if (folderList.Contains(parent))
{
FolderInArrayBlock.Text = "True";
TreeViewItem newChild = new TreeViewItem();
newChild.Header = Name.Text + secondsCount.ToString();
parent.Items.Add(newChild);
}
else
{
FolderInArrayBlock.Text = "false";
TreeViewItem? treeItem = null;
treeItem = new TreeViewItem();
treeItem.Header = Folder.Text;
folderList.Add(treeItem);
treeItem.Items.Add(new TreeViewItem() { Header = Name.Text + secondsCount.ToString()});
LearningItems.Items.Add(treeItem);
}
}
}
}
First of all, if you are using Wpf, you need to use MVVM approch if you want to make a sustainable and maintainable code. This means you need to seperate View funcionalities from Model funcionalities and use a ViewModel as a bridge to be able to communicate with those two things. In Wpf we should try to use Bindings and notifypropertychange to build the brige between View and ViewModel and not use control naming for later use in code behind .cs.(Code behind is the .cs file which belongs to .xaml file ex.: MainWindow.xaml.cs)
I recommend you to take a look at this page, which explains why its so important to use MVVM in your Wpf applications: MVVM pattern
I have created a sample project which demonstrate a proper approch for your task, in my opinion.
MainWindow.xaml
<Window x:Class="TreeViewWithCountDown.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:TreeViewWithCountDown"
xmlns:localviewmodels="clr-namespace:TreeViewWithCountDown.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TreeView ItemsSource="{Binding Path=Items, Mode=OneWay}">
<!--We use TreeView Resources because we bind Items as ItemSource and Items is a List of StorageItems, which can be either FolderItem or FileItem.
TreeView can display the two types differently if we specify in the Resources-->
<TreeView.Resources>
<!--Here we specify how to display a FolderItem-->
<HierarchicalDataTemplate DataType="{x:Type localviewmodels:FolderItem}"
ItemsSource="{Binding Path=Items}">
<TextBlock Text="{Binding Path=Name}"
Margin="0 0 35 0"/>
</HierarchicalDataTemplate>
<!--Here we specify how to display a FileItem-->
<DataTemplate DataType="{x:Type localviewmodels:FileItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="FileNames"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Name}"
Margin="0 0 35 0"
Grid.Column="0"/>
<TextBlock Text="{Binding Path=CountdownTime}"
Margin="0 0 15 0"
Grid.Column="1">
</TextBlock>
</Grid>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
MainWindow.xaml.cs
using System.Windows;
namespace TreeViewWithCountDown
{
public partial class MainWindow : Window
{
private ViewModel _viewModel= new ViewModel();
public MainWindow()
{
InitializeComponent();
//Really important to define where to look for the binding properties
DataContext = _viewModel;
}
}
}
ViewModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Timers;
using TreeViewWithCountDown.ViewModels;
namespace TreeViewWithCountDown
{
public class ViewModel : INotifyPropertyChanged
{
private List<StorageItem> _items = new List<StorageItem>();
public List<StorageItem> Items
{
get => _items;
set
{
if (_items != value)
{
_items = value;
OnPropertyChanged();
}
}
}
public ViewModel()
{
//Filling up our Items property which will be given to the View for display
Random random = new Random();
FileItem item0 = new FileItem("file0", random.Next(0,100));
FolderItem item1 = new FolderItem("folder1");
item1.Items.Add(item0);
FileItem item2 = new FileItem("file2", random.Next(0, 100));
FileItem item3 = new FileItem("file3", random.Next(0, 100));
Timer timer = new Timer(3000);
timer.Elapsed += Time_Elapsed;
timer.Start();
Items.Add(item1);
Items.Add(item2);
Items.Add(item3);
}
private void Time_Elapsed(object sender, ElapsedEventArgs e)
{
foreach (StorageItem item in Items)
{
if (item is FileItem fileItem)
{
fileItem.CountdownTime--;
}
else
{
//Reducing counters of Files in Folders
ReduceFileCountInFolders(item);
}
}
}
//A file can be nested in multiple folders so we can solve this with a recursive method
private void ReduceFileCountInFolders(StorageItem item)
{
if (item is FileItem fileItem)
{
fileItem.CountdownTime--;
}
else if (item is FolderItem folderItem)
{
if (folderItem.Items != null && folderItem.Items.Count > 0)
{
foreach (StorageItem storageItem in folderItem.Items)
{
ReduceFileCountInFolders(storageItem);
}
}
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
try
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
catch (Exception ex)
{
throw new Exception($"PropertyChanged event handler FAILED : {ex.Message}");
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
StorageItem.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace TreeViewWithCountDown.ViewModels
{
public class StorageItem : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
public StorageItem(string name)
{
Name = name;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
try
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
catch (Exception ex)
{
throw new Exception($"PropertyChanged event handler FAILED : {ex.Message}");
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
FileItem.cs
namespace TreeViewWithCountDown.ViewModels
{
public class FileItem : StorageItem
{
private int _countdownTime;
public int CountdownTime
{
get => _countdownTime;
set
{
if (_countdownTime != value && value > 0)
{
_countdownTime = value;
OnPropertyChanged();
}
}
}
public FileItem(string name, int num) : base(name)
{
CountdownTime = num;
}
}
}
FolderItem.cs
using System.Collections.Generic;
namespace TreeViewWithCountDown.ViewModels
{
public class FolderItem : StorageItem
{
private List<StorageItem> _items = new List<StorageItem>();
public List<StorageItem> Items
{
get => _items;
set
{
if (_items != value)
{
_items = value;
OnPropertyChanged();
}
}
}
public FolderItem(string name) : base(name)
{
}
}
}
The final look: View
Hope this will help, if anything seems complicated, feel free to ask!
I have a WPF window that contains multiple user controls, some of which are invisible (Visibility = Hidden). One of these controls has a ComboBox that has an ItemsSource binding, and I want to preset its selected item while the window/control is loading.
However, it seems like the binding is not applied until the combobox is visible. When I go to set the SelectedItem property and I hit a breakpoint in the debugger, I notice that ItemsSource is null at that moment. Is there a way to force WPF to apply the data binding and populate the combobox while it stays invisible?
Reproducible Example:
MainWindow.xaml
<Window x:Class="HiddenComboBoxBinding.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:HiddenComboBoxBinding"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Border x:Name="comboboxParent" Visibility="Collapsed">
<ComboBox x:Name="cmbSearchType" SelectedIndex="0" ItemsSource="{Binding SearchTypeOptions}" DisplayMemberPath="Name" SelectionChanged="cmbSearchType_SelectionChanged" />
</Border>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace HiddenComboBoxBinding
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private ViewModel viewModel { get; set; } = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = viewModel;
// Add some or all of our search types - in the real code, there's some business logic here
foreach (var searchType in SearchType.AllSearchTypes)
{
viewModel.SearchTypeOptions.Add(searchType);
}
// Pre-select the last option, which should be "Bar"
cmbSearchType.SelectedItem = SearchType.AllSearchTypes.Last();
}
private void cmbSearchType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
}
ViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HiddenComboBoxBinding
{
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<SearchType> SearchTypeOptions { get; set; } = new ObservableCollection<SearchType>();
#region INotifyPropertyChanged Members
private void NotifyPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class SearchType
{
// Source list of Search Types
private static List<SearchType> _AllSearchTypes;
public static List<SearchType> AllSearchTypes
{
get
{
if(_AllSearchTypes == null)
{
_AllSearchTypes = new List<SearchType>();
_AllSearchTypes.Add(new SearchType() { Name = "Foo" });
_AllSearchTypes.Add(new SearchType() { Name = "Bar" });
}
return _AllSearchTypes;
}
}
// Instance properties - for the purposes of a minimal, complete, verifiable example, just one property
public string Name { get; set; }
}
}
I was able to figure out the issue. Setting the SelectedItem actually did work (even though ItemsSource was null at that time) but in the XAML, the ComboBox had SelectedIndex="0" and it was taking precedence over the SelectedItem being set in the code-behind.
This question already has answers here:
How do I update an ObservableCollection via a worker thread?
(7 answers)
Closed 5 years ago.
I try to do some simple task (I guess so). I want to change GUI dynamically, from the for loop.
Let's see my 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">
<StackPanel Name="MyPanel">
<TextBlock Text="{Binding MyValue}"></TextBlock>
<Button Click="Button_Click">OK</Button>
<ListBox Name="myList" ItemsSource="{Binding MyCollection}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding A}" Grid.Column="0"/>
<TextBlock Text="{Binding B}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Window>
As you can see, I have the Textblock that shows numbers, button that is starting the program and the listbox, that should show the collection items.
After click on the button, the first textblock (bindes MyValue) shows dynamic values, but on the list box I get the next error:
"This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread."
I saw another answers for the error, but cannot understand how to implement it in my case.
Here the C# code:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public static MyModel m;
public MainWindow()
{
m = new MyModel();
InitializeComponent();
MyPanel.DataContext = m;
}
bool flag = false;
private void Button_Click(object sender, RoutedEventArgs e)
{
flag = !flag;
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 5000000; i++)
{
if (flag == false) break;
m.MyValue = i.ToString();
m.MyCollection.Add(new ChartPoint { A = i, B = 2 * i });
}
});
}
}
public class MyModel : INotifyPropertyChanged
{
private string myValue;
public ObservableCollection<ChartPoint> MyCollection { get; set; }
public MyModel()
{
MyCollection = new ObservableCollection<ChartPoint>();
}
public string MyValue
{
get { return myValue; }
set
{
myValue = value;
RaisePropertyChanged("MyValue");
}
}
private void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ChartPoint
{
public int A { get; set; }
public int B { get; set; }
}
}
Thanks a lot!
I have changed the Button_Click code a bit, is this you want to achieve, please suggest:
private void Button_Click(object sender, RoutedEventArgs e)
{
flag = !flag;
var list = new List <ChartPoint>();
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 50000000; i++)
{
if (flag == false) break;
m.MyValue = i.ToString();
Dispatcher.BeginInvoke(new Action(() =>
{
m.MyCollection.Add(new ChartPoint
{
A = i,
B = 2 * i
});
}),
DispatcherPriority.Background);
}
});
}
I have an issue when I try to use datagrid.items.add() to add items from one datagrid to another. Basically I have two data grids acting in a master slave relationship. The First DataGrid1 is used to display automatically generated columns and rows. The second datagrid, DataGrid2 will display the DataGrid1.SelectedItems when a specific button is clicked. Each time the button is clicked I'd like to have the selected items from DataGrid1 stay in DataGrid2 and each time the button is clicked more items get added to DataGrid2. I have been able to complete most of my requirements with the exception of the ability to edit cells on DataGrid2. When I double click a cell in DataGrid2 I get an exception that says "EditItem' is not allowed for this view". I have read a lot of posts about adding data to a ObservableCollection, ListCollectionView and so on but either I can not implement them in the correct manner or there not working for my situation. My code is as follows and by the way thx in advance
<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 AutoGenerateColumns="False" Height="77" HorizontalAlignment="Left" Margin="27,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="464" />
<Button Content="AddRow" Height="23" HorizontalAlignment="Left" Margin="27,107,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
<DataGrid AutoGenerateColumns="False" Height="140" HorizontalAlignment="Left" Margin="27,159,0,0" Name="dataGrid2" VerticalAlignment="Top" Width="464" />
</Grid>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dataGrid1.ItemsSource = idata;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
foreach (latlongobj item in dataGrid1.SelectedItems)
{
dataGrid2.Items.Add(item);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WpfApplication1
{
class latlonobj
{
public string name { get; set; }
public double Lat { get; set; }
public double Lon { get; set; }
}
}
Is this what you want?
public partial class MainWindow : Window
{
ObservableCollection dataGrid2Items = new ObservableCollection();
public MainWindow()
{
InitializeComponent();
dataGrid1.ItemsSource = idata;
dataGrid2.ItemsSource = dataGrid2Items;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
foreach (latlongobj item in dataGrid1.SelectedItems)
{
if( !dataGrid2Items.Contains( item ) )
dataGrid2Items.Add(item);
}
}
private ObservableCollection<latlongobj> idata = new ObservableCollection<latlongobj>
{
new latlongobj{ name = "n1", Lat = 1, Lon = 2 },
new latlongobj{ name = "n2", Lat = 2, Lon = 3 },
new latlongobj{ name = "n3", Lat = 4, Lon = 5 },
};
My scenario: I have a background thread that polls for changes and periodically updates a WPF DataGrid's ObservableCollection (MVVM-style). The user can click on a row in the DataGrid and bring up the "details" of that row in an adjacent UserControl on the same main view.
When the background thread has updates, it cycles through the objects in the ObservableCollection and replaces individual objects if they have changed (in other words, I am not rebinding a whole new ObservableCollection to the DataGrid, but instead replacing individual items in the collection; this allows the DataGrid to maintain sorting order during updates).
The problem is that after a user has selected a specific row and the details are displayed in the adjacent UserControl, when the background thread updates the DataGrid the DataGrid loses the SelectedItem (it gets reset back to index of -1).
How can I retain the SelectedItem between updates to the ObservableCollection?
If your grid is single-selection, my suggestion is that you use the CollectionView as the ItemsSource instead of the actual ObservableCollection. Then, make sure that Datagrid.IsSynchronizedWithCurrentItem is set to true. Finally, at the end of your "replace item logic", just move the CollectionView's CurrentItem to the corresponding new item.
Below is a sample that demonstrates this. (I'm using a ListBox here though. Hope it works fine with your Datagrid).
EDIT - NEW SAMPLE USING MVVM:
XAML
<Window x:Class="ContextTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="window"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<ListBox x:Name="lb" DockPanel.Dock="Left" Width="200"
ItemsSource="{Binding ModelCollectionView}"
SelectionMode="Single" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Description}"/>
</DockPanel>
</Window>
Code-Behind:
using System;
using System.Windows;
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Windows.Threading;
namespace ContextTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel
{
private DataGenerator dataGenerator;
private ObservableCollection<Model> modelCollection;
public ListCollectionView ModelCollectionView { get; private set; }
public ViewModel()
{
modelCollection = new ObservableCollection<Model>();
ModelCollectionView = new ListCollectionView(modelCollection);
//Create models
for (int i = 0; i < 20; i++)
modelCollection.Add(new Model() { Name = "Model" + i.ToString(),
Description = "Description for Model" + i.ToString() });
this.dataGenerator = new DataGenerator(this);
}
public void Replace(Model oldModel, Model newModel)
{
int curIndex = ModelCollectionView.CurrentPosition;
int n = modelCollection.IndexOf(oldModel);
this.modelCollection[n] = newModel;
ModelCollectionView.MoveCurrentToPosition(curIndex);
}
}
public class Model
{
public string Name { get; set; }
public string Description { get; set; }
}
public class DataGenerator
{
private ViewModel vm;
private DispatcherTimer timer;
int ctr = 0;
public DataGenerator(ViewModel vm)
{
this.vm = vm;
timer = new DispatcherTimer(TimeSpan.FromSeconds(5),
DispatcherPriority.Normal, OnTimerTick, Dispatcher.CurrentDispatcher);
}
public void OnTimerTick(object sender, EventArgs e)
{
Random r = new Random();
//Update several Model items in the ViewModel
int times = r.Next(vm.ModelCollectionView.Count - 1);
for (int i = 0; i < times; i++)
{
Model newModel = new Model()
{
Name = "NewModel" + ctr.ToString(),
Description = "Description for NewModel" + ctr.ToString()
};
ctr++;
//Replace a random item in VM with a new one.
int n = r.Next(times);
vm.Replace(vm.ModelCollectionView.GetItemAt(n) as Model, newModel);
}
}
}
}
OLD SAMPLE:
XAML:
<Window x:Class="ContextTest.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">
<StackPanel>
<ListBox x:Name="lb" SelectionMode="Single" IsSynchronizedWithCurrentItem="True" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="{Binding ElementName=lb, Path=SelectedItem.Name}"/>
<Button Click="Button_Click">Replace</Button>
</StackPanel>
</Window>
Code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace ContextTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ObservableCollection<MyClass> items;
ListCollectionView lcv;
public MainWindow()
{
InitializeComponent();
items = new ObservableCollection<MyClass>();
lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(items);
this.lb.ItemsSource = lcv;
items.Add(new MyClass() { Name = "A" });
items.Add(new MyClass() { Name = "B" });
items.Add(new MyClass() { Name = "C" });
items.Add(new MyClass() { Name = "D" });
items.Add(new MyClass() { Name = "E" });
}
public class MyClass
{
public string Name { get; set; }
}
int ctr = 0;
private void Button_Click(object sender, RoutedEventArgs e)
{
MyClass selectedItem = this.lb.SelectedItem as MyClass;
int index = this.items.IndexOf(selectedItem);
this.items[index] = new MyClass() { Name = "NewItem" + ctr++.ToString() };
lcv.MoveCurrentToPosition(index);
}
}
}
I haven't worked with the WPF DataGrid, but I'd try this approach:
Add a property to the view-model that will hold the value of the currently selected item.
Bind SelectedItem to this new property using TwoWay.
This way, when the user selects a row, it will update the view-model, and when the ObservableCollection gets updated it won't affect the property to which SelectedItem is bound. Being bound, I wouldn't expect it could reset in the way you're seeing.
You could, in the logic that updates the Collection, save off the CollectionView.Current item reference to another variable. Then, after you're done updating, call CollectionView.MoveCurrentTo(variable) to reset the selected item.
Its probably resolved by now, but here is an example of what I did and it works for a grid of carts.
I have a datagrid with ObservableCollection and CollectionView, populated from local variable containing carts:
_cartsObservable = new ObservableCollection<FormOrderCart>(_formCarts);
_cartsViewSource = new CollectionViewSource { Source = _cartsObservable };
CartsGrid.ItemsSource = _cartsViewSource.View;
Later I change Valid prop of carts in a function - not directly, but important is that there is a change in item in ObservableCollection. To reflect the change and maintain selection I just refresh the CollectionViewSource (notice the inside View):
var cart = _formCarts.ElementAt(index-1);
cart.Valid = validity;
_cartsViewSource.View.Refresh();
This way I am able to change the row color in grid to red if the cart is invalid, but also keep my selection.
EDIT: Spelling