I have a panel with tabs. My view model for this panel contains ObservableCollection of view models for tabs, and a property for selected tab.
When some action requests to focus a tab, or a new tab is created, I change Selected and tab selection changes properly, well almost, because the content is valid, but all headers look like nothing is selected.
I found a solution that says to add IsAsync=True to my binding. This solved the problem but added a bunch of new issues.
First thing is that when I run program in debug mode, adding tabs with buttons works ok, tabs get switched and selected properly but when I try to click a tab to select it I get exception
The calling thread cannot access this object because a different thread owns it.
it is thrown while setting property representing currently selected tab:
private Tab selected;
public Tab Selected
{
get { return Selected; }
set { SetProperty(ref Selected, value); } // <<< here (I use prism BindableBase)
}
Other problem is that when I quickly switch tabs, it can come to a situation where I have Tab1 selected but it shows content of Tab2, switching tabs couple more times gets things back to work.
My question is, how can I solve this, i.e. have my tab headers selected (kind of highlighted) when Selected is changed, without having issues that assing IsAsync causes.
Edit
Here is the code that allows to reproduce issues. It uses prism 6.1.0
MainWindow.xaml
<Window x:Class="WpfApplication1.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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Margin="0,5"
Height="25">
<Button
Command="{Binding AddNewTabCommand}"
Content="New Tab"
Padding="10,0"/>
<Button
Command="{Binding OtherCommand}"
Content="Do nothing"
Padding="10,0"/>
</StackPanel>
<TabControl
SelectedItem="{Binding Selected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, IsAsync=True}" <!--remove IsAsync to break tab header selecting-->
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TabGroup();
}
}
Tab.cs
public class Tab : BindableBase
{
public Tab(string name, string text)
{
this.name = name;
this.text = text;
}
private string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
private string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
}
TabGroup.cs
public class TabGroup : BindableBase
{
private Random random;
public TabGroup()
{
this.random = new Random();
this.addNewTabCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(AddNewTab, () => true));
this.otherCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(Method, () => Selected != null).ObservesProperty(() => Selected));
Tabs.CollectionChanged += TabsChanged;
}
private void Method()
{
}
private void TabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var newItems = e.NewItems?.Cast<Tab>().ToList();
if (newItems?.Any() == true)
{
Selected = newItems.Last();
}
}
private void AddNewTab()
{
Tabs.Add(new Tab(GetNextName(), GetRandomContent()));
}
private string GetRandomContent()
{
return random.Next().ToString();
}
private int num = 0;
private string GetNextName() => $"{num++}";
private Tab selected;
public Tab Selected
{
get { return selected; }
set { SetProperty(ref selected, value); }
}
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
private readonly Lazy<DelegateCommand> addNewTabCommand;
public DelegateCommand AddNewTabCommand => addNewTabCommand.Value;
private readonly Lazy<DelegateCommand> otherCommand;
public DelegateCommand OtherCommand => otherCommand.Value;
}
Preparing this let me figure where does the exception come from. It is because the OtherCommand observes selected property. I still don't know how to make it right. Most important for me is to get tabs to be selected when they should be and so that selected tab won't desynchronize with what tab control shows.
Here is a github repo with this code
https://github.com/lukaszwawrzyk/TabIssue
I'll focus on your original problem, without the async part.
The reason why the tabs are not properly selected when adding a new tab is because you set the Selected value in the CollectionChanged event handler. Raising an event causes sequential invocation of handlers in order in which they were added. Since you add your handler in the constructor, it will always be the first one to be invoked, and what's important, it will always be invoked before the one that updates the TabControl. So when you set the Selected property in your handler, TabControl doesn't yet "know" that there's such a tab in the collection. More precisely, the header container for the tab is not yet generated, and it cannot be marked as selected (which causes the visual effect you're missing), moreover, it won't be when it's finally generated. TabControl.SelectedItem is still updated, so you see the content of the tab, but it also causes header container previously marked as selected to be unmarked, and you eventually end up with no tab visibly selected.
Depending on your needs, there are several ways to solve this problem. If the only way of adding new tabs is through the AddNewTabCommand, you could just modify the AddNewTab method:
private void AddNewTab()
{
var tab = new Tab(GetNextName(), GetRandomContent());
Tabs.Add(tab);
Selected = tab;
}
In this case you should not set the Selected value in the CollectionChanged handler, because it will prevent PropertyChanged from being raised at the right time.
If AddNewTabCommand is not the only way of adding tabs, what I usually do is to create a dedicated collection which would do the required logic (this class is nested in TabGroup):
private class TabsCollection : ObservableCollection<Tab>
{
public TabsCollection(TabGroup owner)
{
this.owner = owner;
}
private TabGroup owner;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e); //this will update the TabControl
var newItems = e.NewItems?.Cast<Tab>()?.ToList();
if (newItems?.Any() == true)
owner.Selected = newItems.Last();
}
}
Then simply instantiate the collection in the TabGroup constructor:
Tabs = new TabsCollection(this);
If this scenario appears in various places and you don't like repeating your code, you could create a reusable collection class:
public class MyObservableCollection<T> : ObservableCollection<T>
{
public event NotifyCollectionChangedEventHandler AfterCollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
AfterCollectionChanged?.Invoke(this, e);
}
}
and then subscribe to AfterCollectionChanged whenever you need to be sure that all CollectionChanged subscribers have been notified.
When you get the error "The calling thread cannot access this object because a different thread owns it." this means that you are trying to access an object on another concurrent thread. To show you how to resolve this i want to give an example. First you have to find every runtime objects, like listboxes and listviews and such. (Basically GUI controls). They run on a GUI thread. When you try to run them on another thread forexample a backgroundworker or an task thread, the error appears. So this is what you want to do:
//Lets say i got a listBox i want to update in realtime
//this method is for the purpose of the example running async(background)
public void method(){
//get data to add to listBox1;
//listBox1.Items.Add(item); <-- gives the error
//what you want to do:
Invoke(new MethodInvoker(delegate { listBox1.Items.Add(item); }));
//This invokes another thread, that we can use to access the listBox1 on.
//And this should work
}
Hope it helps.
Related
I've got the simplest Datagrid imaginable, with a button bound to a command that adds a row to the ObservableCollection on which the grid is based. The code is really simple (see the MyData class below, it's just a bunch of lines).
The bug is this:
Add a row
Start editing the Name cell
Press the Add button without exiting from the editing cell
Now the detection of the cell's end editing is broken.
That is, you can go and edit the other cells but setter won't be call for them.
You can go back to the initial editing row, press enter; this sort of fixup things because from now on the editing begin/end return working properly.
In the meantime the content of the edited cells (but the first one) is lost, because the setter has never been called. What you see is only graphical cache so just reorder the rows and puff! Data are gone.
Notice that for reproducing the problem, the Add button has to have the Focusable property set to false, because otherwise it works since clicking on the button makes the Datagrid loosing focus and so when the add-row happens, the cell is not in edit mode.
But being non-focusable is the typical state of ToolBar buttons, infact i first saw this bug with a ToolBar.
Workaround
A workaround is associating and event to the button in the code-behind that ensures that the datagrid is no longer in edit mode:
private void Button_Click(object sender, RoutedEventArgs e)
{
dg.CommitEdit();
}
But is required for all the buttons that potentially fires the addition of a row. I would like something more robust, and I would like to ensure that there is not a problem in adding rows this way (should be the mainstream, but I could miss something the same).
XAML
Just remove the Click="Button_Click" if you don't wanna try the workaround.
<Window x:Class="WpfApp10_DataGridEditingBug.MainWindow"
...
Title="MainWindow" Height="140" Width="280">
<Window.DataContext>
<local:MyData></local:MyData>
</Window.DataContext>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Command="{Binding AddCommand}" Focusable="False" Click="Button_Click">Add</Button>
</StackPanel>
<DataGrid x:Name="dg" ItemsSource="{Binding People}" CanUserAddRows="False">
</DataGrid>
</DockPanel>
</Window>
DataContext
public class MyData
{
public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>();
public RelayCommand AddCommand => addCommand ?? (addCommand = new RelayCommand() { ExecuteAction = AddCommandExecute });
private void AddCommandExecute(object obj)
{
var newPerson = new Person();
People.Add(newPerson);
}
private RelayCommand addCommand;
}
The Row Class
I just an host class for a couple of properties, Name and Surname. I didn't use automatic properties for debugging reasons, this way is easier to spot that it doesn't even call the set method of name property by putting a breakpoint there.
public class Person
{
public string Name
{
get => name;
set => name = value;
}
private string name;
public string Surname
{
get => surname;
set => surname = value;
}
private string surname;
}
And, for completeness, my smart RelayCommand implementation is this:
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public Func<object, bool> CanExecuteFunc;
public Action<object> ExecuteAction;
public bool CanExecute(object parameter) => CanExecuteFunc?.Invoke(parameter) ?? true;
public void Execute(object parameter) => ExecuteAction?.Invoke(parameter);
}
You could set the UpdateSourceTrigger of the bindings to PropertyChanged to set the source properties immediately:
private void dg_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column is DataGridTextColumn textColumn)
textColumn.Binding = new Binding(e.PropertyName) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged };
}
Or you could implement IEditableObject in your Person class.
I have been trying to implement this for a while and haven't been able to do it so far, despite having the feeling that this should be something easy.
The difficulty comes from the fact that I have implemented a WPF application using the MVVM pattern. Now, this is my first attempt at both the pattern and the framework, so it is almost guaranteed that I have made mistakes while trying to follow the MVVM guidelines.
My implementation
I have three Views with their respective ViewModels (wired using Prism's AutoWireViewModel method). The MainView has a TabControl with two TabItems, each of witch contains a Frame container with the Source set to one of the other two Views. The following code is an excerpt of the MainView:
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem Header="Test">
<!--TestView-->
<Frame Source="View1.xaml"/>
</TabItem>
<TabItem Header="Results">
<!--ResultsView-->
<Frame Source="View2.xaml"/>
</TabItem>
</TabControl>
My problem
Every time that someone changes to a specific TabItem, I would like to run a method that updates one of the WPF controls included in that View. The method is already implemented and bound to a Button, but ideally, no button should be necessary, I would like to have some kind of Event to make this happen.
I appreciate all the help in advance.
You could for example handle the Loaded event of the Page to either call a method or invoke a command of the view model once the view has been loaded initially:
public partial class View2 : Page
{
public View2()
{
InitializeComponent();
Loaded += View2_Loaded;
}
private void View2_Loaded(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as ViewModel2;
if (viewModel != null)
viewModel.YourCommand.Execute(null);
Loaded -= View2_Loaded;
}
}
The other option would be handle this in the MainViewModel. You bind the SelectedItem property of the TabControl to a property of the MainViewModel and set this property to an instance of either ViewModel2 or ViewModel2, depending on what kind of view you want to display.
You could then call any method or invoked any command you want on these. But this is another story and then you shouldn't hardcode the TabItems in the view and use Frame elements to display Pages. Please take a look here for an example:
Selecting TabItem in TabControl from ViewModel
Okay, so What I have done is Create a Custom Tab Control. I will write out step by step instructions for this, and then you can add edit to it.
Right click on your solution select add new project
Search For Custom Control Library
High Light the name of the class that comes up, and right click rename it to what ever you want I named it MyTabControl.
Add Prism.Wpf to the new project
Add a reference to the new project to where ever your going to need it. I needed to add to just the main application, but if you have a separate project that only has views then you will need to add it to that too.
Inherit your Custom Control From TabControl Like:
public class MyTabControl : TabControl
You will notice that there is a Themes folder in the project you will need to open the Generic.xaml and edit it. it should look like:
TargetType="{x:Type local:MyTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" for some reason this will not let me show the style tags but they will need to be in there as well
Please review this code I got this from Add A Command To Custom Control
public class MyTabControl : TabControl
{
static MyTabControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTabControl), new FrameworkPropertyMetadata(typeof(MyTabControl)));
}
public static readonly DependencyProperty TabChangedCommandProperty = DependencyProperty.Register(
"TabChangedCommand", typeof(ICommand), typeof(MyTabControl),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandCallBack)));
private static void CommandCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myTabControl = (MyTabControl)d;
myTabControl.HookupCommands((ICommand) e.OldValue, (ICommand) e.NewValue);
}
private void HookupCommands(ICommand oldValue, ICommand newValue)
{
if (oldValue != null)
{
RemoveCommand(oldValue, oldValue);
}
AddCommand(oldValue, oldValue);
}
private void AddCommand(ICommand oldValue, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.TabChangedCommand != null)
{
if (TabChangedCommand.CanExecute(null))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
public ICommand TabChangedCommand
{
get { return (ICommand) GetValue(TabChangedCommandProperty); }
set { SetValue(TabChangedCommandProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TabChangedCommand != null)
{
TabChangedCommand.Execute(null);
}
}
}
you will need to add the name space in your window or usercontrol like:
xmlns:wpfCustomControlLibrary1="clr-namespace:WpfCustomControlLibrary1;assembly=WpfCustomControlLibrary1"
and here is your control:
<wpfCustomControlLibrary1:MyTabControl TabChangedCommand="{Binding TabChangedCommand}">
<TabItem Header="View A"></TabItem>
<TabItem Header="View B"></TabItem>
</wpfCustomControlLibrary1:MyTabControl>
This is how I'd approach this sort of requirement:
View:
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="tc" ItemsSource="{Binding vms}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:uc1vm}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:uc2vm}">
<local:UserControl2/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabHeading}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
When it has a uc1vm it will be templated into usercontrol1 in the view.
I'm binding to a collection of viewmodels which all implement an interface so I know for sure I can cast to that and call a method.
Main viewmodel for window:
private IDoSomething selectedVM;
public IDoSomething SelectedVM
{
get { return selectedVM; }
set
{
selectedVM = value;
selectedVM.doit();
RaisePropertyChanged();
}
}
public ObservableCollection<IDoSomething> vms { get; set; } = new ObservableCollection<IDoSomething>
{ new uc1vm(),
new uc2vm()
};
public MainWIndowViewModel()
{
}
When a tab is selected, the setter for selected item will be passed the new value. Cast that and call the method.
My interface is very simple, since this is just illustrative:
public interface IDoSomething
{
void doit();
}
An example viewmodel, which is again just illustrative and doesn't do much:
public class uc1vm : IDoSomething
{
public string TabHeading { get; set; } = "Uc1";
public void doit()
{
// Your code goes here
}
}
I appreciate all of your input, but I found an alternative solution. Given the information given by #mm8, I took advantage of the Loaded event but in a way that does not require any code in the code behind.
My solution
In the View which I would like to give this ability to execute a method every time the user selects the TabItem that contains it, I added the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And then simply implemented a DelegateCommand called OnLoadedCommand in the View's respective ViewModel. Inside that command I call my desired method.
Please comment if you spot anything wrong with this approach! I chose to try this since it required the least amount of changes to my code, but I may be missing some vital information regarding problems the solution may cause.
I have a custom control to show items with checkboxes inside a ComboBox. To realize this, I used a DataTemplate with a CheckBox. The ItemSource of the ComboBox uses a binding to a ObserableCollection<FilterValue> which contains my filter values. FilterValue is a custom class implementing INotifyPropertyChanged. The properties Content and IsChecked of the CheckBox use bindings as well to use the values of my list. This control will be used in Silverlight.
Binding itself works fine, as seen here:
The problem appears when I register the Checked or Unchecked event.
As soon as one of the check boxes changed its state, the event is fired as expected but at this moment, the value in the bound list is still not updated.
What I saw while debugging is that the Checked/Unchecked events are firing before the PropertyChanged event of the FilterValue.
This means that at the time the event is firing, I can't ask the list for all active (checked) filters. What could I do to achieve this?
FilterControl.xaml:
<UserControl
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:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:local="clr-namespace:Controls" x:Class="Controls.FilterControl"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="140">
<StackPanel x:Name="LayoutRoot">
<sdk:Label x:Name="LblFilterDescription" Content="-" />
<ComboBox x:Name="Filter" Width="120" ItemsSource="{Binding AvailableFilters, RelativeSource={RelativeSource FindAncestor, AncestorType=local:FilterControl}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Path=Text}" IsChecked="{Binding Path=IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Checked="FilterChanged" Unchecked="FilterChanged" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</UserControl>
FilterControl.xaml.cs:
public partial class FilterControl : UserControl
{
public delegate void FilterChangedHandler(object sender);
public event FilterChangedHandler OnFilterChanged;
public ObservableCollection<FilterValue> AvailableFilters { get; set; }
public List<string> AppliedFilters
{
get
{
return new List<string>(AvailableFilters.Where(filter => filter.IsChecked).Select(filter => filter.Text));
}
}
public FilterControl()
{
InitializeComponent();
AvailableFilters = new ObservableCollection<FilterValue>();
}
public bool AddFilterValue(string filterValue)
{
bool found = false;
foreach (FilterValue f in AvailableFilters)
{
if (f.Text == filterValue)
{
found = true;
break;
}
}
if (!found)
AvailableFilters.Add(new FilterValue() { IsChecked = false, Text = filterValue });
return found;
}
private void FilterChanged(object sender, RoutedEventArgs e)
{
//Here if I check AvailableFilters, the value is not changed yet.
//PropertyChanged allways fires after this, what makes me unable to
//get all currently applied filters (checked items)...
}
}
FilterValue:
public class FilterValue : INotifyPropertyChanged
{
private bool _IsChecked;
private string _Text;
public bool IsChecked
{
get { return _IsChecked; }
set
{
_IsChecked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public string Text
{
get { return _Text; }
set
{
_Text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Text"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So, as I tried to reproduce this behavior, I realized that this appears to be a behavior that only occurs like that in Silverlight. If you try this example on WPF, the Changed fires after the bound property is updated. So you can just access your AppliedFilters property in the FilterChanged method and it will reflect the actual current situation. On Silverlight though, not so much. Even worse, this behavior didn’t even appear to be consistent to me. I did encounter situations in which the event fired after the property has been updated (resulting in the expected output).
A way to get around this is to clean up your component logic. If you look at it, you are mixing two different concepts: Event-driven UI logic, and clear data binding. Of course, doing it “properly” has multiple effects you likely cannot just ensure in an existing project, but you can at least try to get in the right direction here which should then also solve this issue.
So your logic right now uses data binding to provide the data for the view, and to reflect changes of the displayed items. But you are using events on the item level to perform additional logic depending on the former changes. As we have seen, the order of execution appears not be guaranteed across platforms, so it’s best to avoid having to rely on it.
In this case, you should have your data be the source of truth and make changes in the data tell you when applied filters change. You’re already halfway there by having an ObservableCollection and items that implement INotifyPropertyChanged. Unfortunately, an observable collection will only notify you about changes to the collection but not to changes to the contained items. But there are multiple solutions to expand the collection to also look at the items inside the collection.
This related question covers exactly that topic and there are multiple ideas on how to expand the observable collection for exactly that behavior. In my case, I have used the FullyObservableCollection implementation by Bob Sammers.
All you have to do for that is to change your ObservableCollection<FilterValue> into a FullyObservableCollection<FilterValue> and subscribe to the ItemPropertyChanged event:
AvailableFilters = new FullyObservableCollection<FilterValue>();
AvailableFilters.ItemPropertyChanged += AvailableFilters_ItemPropertyChanged;
In that event handler, you will then correctly see the proper behavior.
I have a large collection of items bound to a ListBox, with a VirtualizingStackPanel set as its ItemsPanel. As the user scrolls and item containers are created, I do some work to populate the item with data (using a database query). If the user scrolls very rapidly, it builds up a large number of requests that tend to bog things down. What I would like to do is detect when the item is scrolled outside of the viewport, so I can cancel its corresponding request.
Here are the approaches I've tried thus far, and why they have not worked:
Override VirtualizingStackPanel.OnCleanUpVirtualizedItem. The
problem is that this method seems to be called sometime much later
than when the item actually goes off-screen. Cancelling my request
within this method doesn't do much good because it occurs so late.
Turn on container recycling with
VirtualizationMode.Recycling. This event causes the item
container's DataContext to change but the item container itself is
reused. The DataContextChanged event occurs immediately as one
item goes outside of view, so it is good in that regard. The problem
is that container recycling creates a lot of side-effects and, in my
testing, is a little buggy overall. I would prefer not to use it.
Is there are a good lower-level approach, such as hooking into layout events, that can give me a deterministic answer on when an item goes outside of view? Perhaps at the ScrollViewer level?
Here's a rough solution that I think accomplishes what you're looking for. I'm getting the virtualizing stack panel by listening to the loaded event in the XAML. If I were doing this in production code, I might factor this into a reusable attached behavior rather than throwing a bunch of code in the code-behind.
public partial class MainWindow
{
private VirtualizingStackPanel _panel;
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
private IList<ChildViewModel> _snapshot = new List<ChildViewModel>();
private void OnPanelLoaded(object sender, RoutedEventArgs eventArgs)
{
_panel = (VirtualizingStackPanel)sender;
UpdateSnapshot();
_panel.ScrollOwner.ScrollChanged += (s,e) => UpdateSnapshot();
}
private void UpdateSnapshot()
{
var layoutBounds = LayoutInformation.GetLayoutSlot(_panel);
var onScreenChildren =
(from visualChild in _panel.GetChildren()
let childBounds = LayoutInformation.GetLayoutSlot(visualChild)
where layoutBounds.Contains(childBounds) || layoutBounds.IntersectsWith(childBounds)
select visualChild.DataContext).Cast<ChildViewModel>().ToList();
foreach (var removed in _snapshot.Except(onScreenChildren))
{
// TODO: Cancel pending calculations.
Console.WriteLine("{0} was removed.", removed.Value);
}
_snapshot = onScreenChildren;
}
}
Notice that there isn't really a property we can use here to find the on-screen children, so we look at the layout bounds of the parent compared to the children to determine which children are on screen.
The code uses an extension method for getting the visual children of an item in the visual tree, included below:
public static class MyVisualTreeHelpers
{
public static IEnumerable<FrameworkElement> GetChildren(this DependencyObject dependencyObject)
{
var numberOfChildren = VisualTreeHelper.GetChildrenCount(dependencyObject);
return (from index in Enumerable.Range(0, numberOfChildren)
select VisualTreeHelper.GetChild(dependencyObject, index)).Cast<FrameworkElement>();
}
}
This code is using a very basic view model hierarchy I created for the purposes of testing this out. I'll include it just in case it's helpful in understanding the other code:
public class MyViewModel
{
public MyViewModel()
{
Children = new ObservableCollection<ChildViewModel>(GenerateChildren());
}
public ObservableCollection<ChildViewModel> Children { get; set; }
private static IEnumerable<ChildViewModel> GenerateChildren()
{
return from value in Enumerable.Range(1, 1000)
select new ChildViewModel {Value = value};
}
}
public class ChildViewModel
{
public int Value { get; set; }
}
XAML:
<Window x:Class="WpfTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfTest="clr-namespace:WpfTest"
Title="MainWindow" Height="500" Width="500">
<ListBox ItemsSource="{Binding Children}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Loaded="OnPanelLoaded" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="wpfTest:ChildViewModel">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
On the viewmodel side you could watch attach and detach from INotifyPropertyChanged event:
public event PropertyChangedEventHandler PropertyChanged
{
add
{
if(this.InternalPropertyChanged == null)
Console.WriteLine("COMING INTO VIEW");
this.InternalPropertyChanged += value;
}
remove
{
this.InternalPropertyChanged -= value;
if(this.InternalPropertyChanged == null)
Console.WriteLine("OUT OF VIEW");
}
}
private event PropertyChangedEventHandler InternalPropertyChanged;
Note: Without VirtualizationMode.Recycling a ListBox may defer the container destruction (and hence the detach) until the user stops scrolling. This can increase memory consumption extensivly, especially if the ItemTemplate is complex (and also won't cancel your DB queries).
You can possibly try to add.
scrollviewer.veriticalscroll = "auto"
That will cause it to only scroll for the amount of items already in the listbox
I have made a tree View in wpf Using MVVM .
it is working fine but here is one problem that leaf node contains some checkboxes and user have only two options either to select one or none .
So here how i can restricted user to select maximum only one cold drink.
I did one trick but it didn't work that when i have already selected a drink and then i select another one than i set the last selected value in the observable collection to false but it doesn't affect on view and selected check boxes remains selected although in collection only one option's value is true.
I cant use radio button instedof checkbox becasue user can select none of the options and i cant give an additional option for none of the above.
If any one have any solution so please let me know I'll be very thankful.
updated question:
i think i didn't define my problem in a proper way so i am giving my code snipperts here hope by this i'll get the solution o f my problem...
My View Model Class
namespace TestViewModels
{
public class ViewModel :ViewModelBase
{
private ObservableCollection<AvailableProducts> _MyTreeViewProperty
public ObservableCollection<AvailableProducts> MyTreeViewProperty
{
get { return _MyTreeViewProperty
set { _MyTreeViewProperty value;
RaisePropertyChanged("MyTreeViewProperty");}
}
}
public class AvailableProducts
{
private string _BrandName;
public string BrandName
{
get { return _BrandName
set { _BrandName = value; }
}
private bool _IsExpanded;
public bool IsExpanded
{
get
{
return _IsExpanded;
}
set
{
_IsExpanded = value;
}
}
private ObservableCollection<ProductTypes> _MyProductTypes
public ObservableCollection<ProductTypes> MyProductTypes
{
get { return _MyProductTypes}
set { _MyProductTypes= value; }
}
}
public class ProductTypes
{
private string _ProductTypeName;
public string ProductTypeName
{
get { return _ProductTypeName;
set { _ProductTypeNamevalue; }
}
private ObservableCollection<ProductSubTypes> _ProdSubTypes;
public ObservableCollection<ProductSubTypes> ProdSubTypes
{
get { return _ProdSubTypes;}
set { _ProdSubTypes;= value; }
}
}
public class ProductSubTypes
{
private string _ProductSubTypeName;
public string ProductSubTypeName
{
get { return _ProductSubTypeName;
set { _ProductSubTypeName;}
}
private int _ParentID;
public int ParentID
{
get { return _ParentID;}
set { _ParentID;= value; }
}
private bool _IsAssigned;
public bool IsAssigned
{
get { return _IsAssigned; }
set
{
_IsAssigned = value;
if _ParentID;!= 0)
{
//updating data in database
//Calling and setting new collection value in property
//issue : updated collection sets in setter of MyTreeViewProperty but before calling getter
// it comes to IsAssigned getter so view doesnt get updated collection of MyTreeViewProperty
}
RaisePropertyChanged("IsAssigned");
}
}
}
}
View
<Page x:Class="ShiftManagerViews.Pages.ProductTreeSelection
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"
DataContext="{Binding ProductsTree, Source={StaticResource Locator}}"
mc:Ignorable="d" Width="870" Height="665"
>
<TreeView Margin="10,10,0,13" ItemsSource="{Binding MyTreeViewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="800" Height="Auto" MinHeight="400" MaxHeight="800">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:AvailableProducts}"
ItemsSource="{Binding MyProductTypes}">
<WrapPanel>
<Image Width="20" Height="20" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding BrandName}" FontSize="14"/>
</WrapPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ProductTypes}"
ItemsSource="{Binding ProdSubTypes}">
<WrapPanel>
<Image Width="18" Height="15" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding ProductTypeName}" FontSize="13"/>
</WrapPanel>
</HierarchicalDataTemplate>
<!-- the template for showing the Leaf node's properties-->
<DataTemplate DataType="{x:Type local:ProductSubTypes}">
<StackPanel>
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}" Content="{Binding ProductSubTypeName}" Height="25">
</CheckBox>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
What about using a ListBox to display sub-items instead of a TreeView? You can style that so the items contain a CheckBox to show IsSelected instead of highlighting the item.
I'd suggest your user interface is wrong. If the user can only pick one then it would be better to swap these for radio buttons and add a "None of the above" option. That'll then give you the behaviour you want for free and your UI will be more intuitive.
EDIT: Since you say you can't add a "None" option and want to use a checkbox (even though I strongly disagree on checkboxes where a radio button is more appropriate - a common UI error)...
The technical problem you are probably facing is that an ObservableCollection only raises notification events if the collection itself changes. i.e. Only if items are added or removed. It does not raised events when items within the collection change, therefore the changing the status of the checkbox in the code will not raise the event for the UI binding to act on.
One solution to this to write a custom class that extends ObservableCollection that does provide this behaviour
From MSDN:
If you need to know if someone has changed a property of one of the
items within the collection, you'll need to ensure that the items in
the collection implement the INotifyPropertyChanged interface, and
you'll need to manually attach property changed event handlers for
those objects. No matter how you change properties of objects within
the collection, the collection's PropertyChanged event will not fire.
As a matter of fact, the ObservableCollection's PropertyChanged event
handler is protected—you can't even react to it unless you inherit
from the class and expose it yourself. You could, of course, handle
the PropertyChanged event for each item within the collection from
your inherited collection
I upvoted Rachel's answer, it is a common way in WPF to databind sets of radio buttons or check boxes. If you still want to go the tree view way, below code works. All view related code is in the view, so below code follows MVVM principles. If you are a MVVM purist you can put the code behind and a TreeView control in a user control if you do not want any code behind.
XAML:
<TreeView ItemsSource="{Binding Path=Drinks}">
<TreeView.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding .}" Checked="OnCheckBoxChecked" Unchecked="OnCheckBoxUnchecked" Loaded="OnCheckBoxLoaded" />
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind + VM:
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
private void OnCheckBoxChecked(object sender, System.Windows.RoutedEventArgs e)
{
foreach (CheckBox checkBox in _checkBoxes.Where(cb => cb != sender))
{
checkBox.IsChecked = false;
}
(DataContext as VM).CurrentDrink = (sender as CheckBox).Content.ToString();
}
private void OnCheckBoxUnchecked(object sender, System.Windows.RoutedEventArgs e)
{
(DataContext as VM).CurrentDrink = null;
}
private void OnCheckBoxLoaded(object sender, System.Windows.RoutedEventArgs e)
{
_checkBoxes.Add(sender as CheckBox);
}
private List<CheckBox> _checkBoxes = new List<CheckBox>();
}
public class VM
{
public List<string> Drinks
{
get
{
return new List<string>() { "Coffee", "Tea", "Juice" };
}
}
public string CurrentDrink { get; set; }
}
I did one trick but it didn't work that when i have already selected a
drink and then i select another one than i set the last selected value
in the observable collection to false but it doesn't affect on view
and selected check boxes remains selected although in collection only
one option's value is true.
Make sure that your child objects (AvailableProducts
and SubProductTypes) also implement INotifyPropertyChanged, this will make sure that the UI receives changes when modify the object.
Once all of you objects update the UI properly you will be able to layer in, and test, whatever custom business logic you need.
So if you have a product type that can only have one sub chosen, you could add a property on ProductType called OnlyAllowOneChild. Whenever, a child object raises a IsAssigned changed event, the parent can set false all other children. This of course requires you to have the parent either register for the children's PropertyChangedEvent, or got grab an EventAggregator (MVVMLight Messenger, or PRISM EvenAggregator) and create a messaging system.
Finally i am succeeded to solve my problem.
on Is Assigned property i am updating my database values and calling a method in view using MVVM Light messaging and passing currently selected leaf's parent id in it as a parameter...
Added a property in class Product Types to expand the parent node of the last selected leaf..
In view's method i am refreshing data context's source and passing currently selected leaf's parent id tO the VM to set its Is Expanded property value to true...
By this my view is working perfectly as same as i want...
If any body have solution better than this than I'll be happy to know.