I want to change a complete TabControl based on the ListItem selection. So, I generate a Dictionary with Key(list item) and Value(TabControl) in a loop. Each TabControl contains 10 TabItems.
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
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 WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ListboxContainer listBoxContainer = new ListboxContainer();
TabControlContainer tabControlContainer = new TabControlContainer();
Dictionary<string, TabControl> tabControlDict = new Dictionary<string, TabControl>();
public MainWindow()
{
InitializeComponent();
listBoxContainer.ListBoxItems = new string[] { "TabControl1", "TabControl2", "TabControl3" };
DataContext = listBoxContainer;
// Generate tabcontrols per listbox item
foreach (var key in listBoxContainer.ListBoxItems)
{
TabControl tabControl = new TabControl();
TabItem tabItem = new TabItem();
for (int i = 0; i < 10; i++)
{
tabItem.Header = $"Header: {i}";
}
tabControl.Items.Add(tabItem);
tabControlDict.Add(key, tabControl);
}
tabControlContainer.TabControls = tabControlDict.Values.ToArray();
}
}
public class TabControlContainer : INotifyPropertyChanged
{
private TabControl[] _tabControls;
public TabControl[] TabControls
{
get => _tabControls;
set
{
if (value != _tabControls)
{
_tabControls = value;
NotifyPropertyChanged("TabControls");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ListboxContainer : INotifyPropertyChanged
{
private string[] _listBoxitems;
public string[] ListBoxItems
{
get => _listBoxitems;
set
{
if (value != _listBoxitems)
{
_listBoxitems = value;
NotifyPropertyChanged("ListBoxItems");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I tried to bind the stackpanel to the tabcontrols, but I can't get it to work.
Here is the MainWindow.xaml code.
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<ListBox x:Name="Listbox" ItemsSource="{Binding ListBoxItems}"/>
<StackPanel x:Name="TabControlContainer">
<ItemsControl ItemsSource="{Binding TabControls}">
<!--<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>-->
</ItemsControl>
</StackPanel>
</DockPanel>
</Window>
So, basically, I want to achieve a Master/Detail view, where Master is the listbox and Detail is its belonding TabControl.
I know, that I can manually add/delete children of the Stackpanel, based on list item selection, but it seems to me not the MVVM way.
You want a ListBox and a TabControl.
Both controls you have there are itemscontrols.
They both inherit from Selector so they have SelectedItem.
Bind a collection of viewmodels to your listbox, call these something meaningful. PersonViewModel or FruitViewModel or something. Don't call them listboxitem or anything that is the same name or anything like the name of a UI control. One per row. But let's call them RowVM as in a viewmodel holding the data for a listbox row.
If you bind to SelectedItem of that ListBox you get an instance of RowVM.
You can bind that from your listbox to a property in your WindowViewModel. You can also bind to that property from some other control.
Let's go back to meaningful names though.
Maybe a Person has Shoes. A collection of Shoe that you're going to show in that tabcontrol. Choose a Person in the listbox and you see their shoes in the tabcontrol. So PersonViewModel would have an observablecollection of shoe.
You can bind itemssource of that tabcontrol to Shoes of the selected item of the listbox. Which will then grab that collection of shoes for whichever person you choose in the listbox.
That'll be quite a clunky sort of a binding though and binding both to a property in your viewmodel is usually cleaner.
If you bind that listbox to a collectionview, you can alternatively use the current item / notation in your binding.
https://social.technet.microsoft.com/wiki/contents/articles/29859.wpf-tips-bind-to-current-item-of-collection.aspx
And of course that'll just give you the type name because it doesn't know how to display a shoe.
But that's no problem because wpf has a whole data templating system. That allows you to give it a template to use for a shoe.
You should be able to google examples of all that stuff but here's one on the tabcontrol specifically:
How do I bind a TabControl to a collection of ViewModels?
I hope that's enough to point you in the right direction. Or at least along a path with less rocks.
Related
I am new to creating apps. I am trying to build a WPF app that requires me to have a combobox with hundreds of items. I have all of these items saved in a txt/excel file. Obviously, I don't want to hardcode all the options in XAML/C#.
I have no idea how to go about this. I tried to store the list as a resource but don't know how to access the resource once I've put it in there.
I am looking for the easiest approach that can accomplish what I need.
Thanks!
To get the values in your txt/csvfile is trivial (What's the fastest way to read a text file line-by-line?). To get these values to update your ComboBox's contents is actually fairly involved, but the process is at the heart of WPF and MVVM.
The basic idea is to bind an ObservableCollection<string> object to the ItemSource property in your ComboBox and fill it with the items in your text file. I read the file in the constructor of my view model (more on that below) and put all the lines in the Collection (which is bound to the box) on startup. You can also do this elsewhere dynamically if need be.
The usual way to wire it up to the GUI is to use a DataContext in your MainWindow.xaml file. Typically this is done using the Model View ViewModel (MVVM) pattern. The ViewModel is responsible for communicating between the business logic (Model) and the GUI (View). In WPF this is done by having the ViewModel handle event changes in the GUI and also notify the GUI when the data from the model changes. Here I do this with a class called Notifier that implements INotifyPropertyChanged. Then, ViewModel inherits this class and can talk to the GUI via bindings in the xaml. There are other ways of doing the View notifying in WPF/MVVM, but I find this is the simplest. Every WPF project I work on has this class in it.
You will also need bindings for the SelectedItem and SelectedIndex properties in your ComboBox. By binding these to properties that notify in your view model (via calling Update in the setter) you can control all the behavior you need.
Note that I had to remove the StartupUri="MainWindow.xaml" line in the App.xaml file because I instantiate the MainWindow object in the code behind.
Here is how I did it using the standard Visual Stduio WPF application template. Just to clarify how this conforms to MVVM: the 'View' is the GUI itself (defined in MainWindow.xaml), the 'ViewModel' is the ViewModel class, and the 'Model is trivial. It is just a static set of items from the text file to shove into the combobox.
App.xaml:
<Application x:Class="ComboBoxDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ComboBoxDemo">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.cs:
using System.Windows;
namespace ComboBoxDemo
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
ViewModel viewModel = new();
MainWindow mainWindow = new() { DataContext = viewModel };
mainWindow.Show();
}
}
}
MainWindow.xaml:
<Window x:Class="ComboBoxDemo.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:ComboBoxDemo"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:ViewModel}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox Grid.Column="0"
ItemsSource="{Binding Items, Mode=OneWay}"
SelectedItem="{Binding SelectedItem, Mode=OneWay}"
SelectedIndex="{Binding ItemIndex}"
Margin="5"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboBoxDemo
{
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent();
}
}
ViewModel.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
namespace ComboBoxDemo
{
public abstract class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = null;
protected void Update<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChangedEventHandler? handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ViewModel : Notifier
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
// event handling for gui/business logic
// private fields for bound variables
private string _selectedItem = "";
private int _itemIndex = 0;
// properties for binding to GUI
public ObservableCollection<string> Items { get; } = new();
public string SelectedItem
{
get => _selectedItem;
set => Update(ref _selectedItem, value);
}
public int ItemIndex
{
get => _itemIndex;
set
{
Update(ref _itemIndex, value);
// here you can use the index to affect business logic as well
}
}
public ViewModel()
{
Items = new();
string textFile = "items.txt";
using StreamReader file = new StreamReader(textFile);
string? line;
while ((line = file.ReadLine()) is not null)
Items.Add(line);
}
}
}
and my combobox items come from 'items.txt' which I put in the same directory as the .exe:
item1
item2
item3
Screenshot of output:
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.
I have a TabControl with it's ItemsSource bound to a ObservableCollection<string>. In this case, the TabControl has no logical children (LogicalTreeHelper.GetChildren(tabctrl) returns an empty list).
If i add a TabItem manually to the TabControl.Items collection, the TabItem is a logical child of the TabControl.
Why do these ways behave differently? Shouldn't the TabControl have a logical child in both scenarios?
Example code:
XAML
<Window x:Class="WpfApplication29.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>
<TabControl Name="tabctrl"/>
<Button Content="count children" Click="Button_Click_2"/>
</StackPanel>
</Window>
code behind
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfApplication29
{
public partial class MainWindow : Window
{
public ObservableCollection<string> TabItems
{
get { return (ObservableCollection<string>)GetValue(TabItemsProperty); }
set { SetValue(TabItemsProperty, value); }
}
public static readonly DependencyProperty TabItemsProperty =
DependencyProperty.Register("TabItems", typeof(ObservableCollection<string>), typeof(MainWindow), new PropertyMetadata(null));
public MainWindow()
{
InitializeComponent();
TabItems = new ObservableCollection<string>();
TabItems.Add("foo");
//scenario 1 (Visual Children count: 1, logical children count: 0)
tabctrl.SetBinding(TabControl.ItemsSourceProperty, new Binding("TabItems") { Source = this });
//scenario 2 (Visual Children count: 1, logical children count: 1)
//tabctrl.Items.Add(new TabItem() { Header = "bar", Content = "bar" });
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
var visualChildrenCount = VisualTreeHelper.GetChildrenCount(tabctrl);
var logicalChildrenCount = LogicalTreeHelper.GetChildren(tabctrl).Cast<object>().Count();
MessageBox.Show(string.Format("Visual Children: {0}, Logical Children: {1}", visualChildrenCount, logicalChildrenCount));
}
}
}
if you add a control to your usercontrol, or page, it is added to its LogicalTree. However, the UIElements from the control's template are not part of the LogicalTree.
IMHO, when you add TabItem directly to TabControl, you expect to be present in LogicalTree, intuitively. You have direcly added it there.
But when the items are generated from itemssource, the TabItems are generated by the control's internal logic and are not added to LogicalTree.
Maybe better example is ListBox, or DataGrid. Imagine that you have ItemsSource bound to very large collection and you need to enable virtualization. Then items (UIElements) are generated, only when needed (they are in visible area of the scrollviewver). If they were in logical tree, the logical tree would be changing while scrolling. but scrolling is more about visual than the "UI logic".
this article helps to understand the logical tree a little bit better: http://www.codeproject.com/Articles/21495/Understanding-the-Visual-Tree-and-Logical-Tree-in
I'm very new to programming in Visual Studio and for Windows Phone 8. I do have knowledge in PHP, so I understand the basics of syntax, albeit a slight differentiation from C#, however.
Anyways, I am simply trying to make a list I've declared in a variable in \MainPage.xaml.cs viewable in \MainPage.xaml, via binding, I guess that's how it's done.
I'm trying to make it as basic as possible for now; here is what I have in the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using lbpme_viewer.Resources;
namespace lbpme_viewer
{
public partial class MainPage : PhoneApplicationPage
{
List<String> MenuItems = new List<String> { "portal", "news", "myprofile", "settings" };
// Constructor
public MainPage()
{
InitializeComponent();
}
}
}
And I would just make 4 plain textblocks, but I want to remove "myprofile" based on the user's settings, but before I get to that, right now I just declared them all in one list of strings.
And a portion of my XAML file:
<!--Panorama item one-->
<phone:PanoramaItem Header="first item">
<!--Single line list with text wrapping-->
<phone:LongListSelector x:Name="menuList" Margin="0,0,-22,0" ItemsSource="{Binding Path=MenuItems}">
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,-6,0,12">
<TextBlock Text="{Binding}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}" FontSize="{StaticResource PhoneFontSizeExtraLarge}"/>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</phone:PanoramaItem>
And obviously the items won't show in the LongListSelector. I don't expect them to because I know I'm missing one or many pieces of code required to do so, and my question is, how?
Again, I just want the list of phrases in the MenuList variable to appear in my LongListSelector, like how "design one" "design two" does in the default panorama/pivot app upon creation.
And if it wouldn't be much different, how would I get any variable from the C# code to appear in the XAML file?
Thank you!
Try binding to an ObservableCollection<string> that implements INotifyPropertyChanged like this
List<String> MenuItemsList = new List<String> { "portal", "news", "myprofile", "settings" };
private ObservableCollection<string> _menuItems;
public ObservableCollection<string> MenuItems
{
get { return _menuItems; }
set
{
if (_menuItems == value) return;
_menuItems = value;
NotifyPropertyChanged(); // or NotifyPropertyChanged("MenuItems");
}
}
then in your constructor new up the ObservableCollection by passing in the List
MenuItems = new ObservableCollection<string>(MenuItemsList);
You also need this in your class (from http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged(v=vs.110).aspx)
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Take a look at MVVM Light, it makes a lot of WPF things easier. With MVVM Light, your property could look like this (and you wouldn't need all the NotifyPropertyChanged stuff)
private ObservableCollection<string> _menuItems;
public ObservableCollection<string> MenuItems
{
get { return _menuItems; }
set
{
if (_menuItems == value) return;
_menuItems = value;
RaisePropertyChanged(() => MenuItems);
}
}
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