Selection changing event in WPF - wpf

I am looking for a way to prevent a selection change in WPF items (the Tab control right now, but in the future this will need to be done for ListBoxes, ListViews and ComboBoxes).
I came across this thread and attempted to use the same technique that was marked as the answer.
In that technique you retrieve the CollectionView for the tab control's items and handle the CollectionView's CurrentChanging event to prevent the selection from occurring.
For some reason, the CurrentChanging event is never being fired in my code.
Here is my very simple user control that I am working with.
It has a tab control with 3 tabs.
(XAML)
<UserControl x:Class="UserControlWithTabs"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TabControl x:Name="MainTabControl">
<TabItem Header="First Tab">Content for the first tab</TabItem>
<TabItem Header="Second Tab">Content for the second tab</TabItem>
<TabItem Header="Third Tab">Content for the third tab</TabItem>
</TabControl>
</UserControl>
In my VB.NET code for the user control, I am simply retrieving the CollectionView for the tab control's items and using the AddHandler method to watch for the event.
(VB.NET)
Public Class UserControlWithTabs
Private WithEvents mainTabCollectionView As CollectionView
Private Sub UserControlWithTabs_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded
mainTabCollectionView = CollectionViewSource.GetDefaultView(MainTabControl.Items)
AddHandler mainTabCollectionView.CurrentChanging, AddressOf MainTabControl_ItemSelecting
End Sub
Private Sub MainTabControl_ItemSelecting(ByVal sender As Object, ByVal e As System.ComponentModel.CurrentChangingEventArgs)
End Sub
End Class
I put a break point on the MainTabControl_ItemSelecting method, but it is never hit.
What am I doing wrong?
Thanks,
-Frinny

Have you tried adding IsSynchronizedWithCurrentItem="True" to your TabControl?

Thanks to both the question and answer, i was able to do this in c#.
So for anyone needing something like this with c# code-behind, here's how i did it:
mytab.IsSynchronizedWithCurrentItem = true;
mytab.Items.CurrentChanging += new CurrentChangingEventHandler(Items_CurrentChanging);
private void Items_CurrentChanging(object sender, CurrentChangingEventArgs e)
{
if (e.IsCancelable)
{
FrameworkElement elemfrom = ((ICollectionView)sender).CurrentItem as FrameworkElement;
FrameworkElement elemto = mytab.SelectedItem as FrameworkElement;
}
Console.WriteLine("tab is changing.");
}

Related

WPF Cancel tab selection with message box

I have a need to detect a dirty page when the user tries to move to a new tab and give the user the option to cancel the move off the current tab. I can get this to work when I don't need to ask the user, but showing the messagebox is breaking the functionality. I have provided a simple app that exhibits the problem. Sorry it's in VB, but that's what we use here.
<Window x:Class="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:ChangeTab"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<TabControl SelectedIndex="{Binding SelectedIndex}" Initialized="TabControl_Initialized" IsSynchronizedWithCurrentItem="True">
<TabItem Header="Tab 1">
<TextBlock Text="Content 1"/>
</TabItem>
<TabItem Header="Tab 2">
<StackPanel>
<TextBlock Text="Content 2"/>
<CheckBox Content="Locked" IsChecked="{Binding IsChecked}"/>
</StackPanel>
</TabItem>
</TabControl>
</Window>
Imports System.ComponentModel
Imports System.Windows.Threading
Class MainWindow
Implements INotifyPropertyChanged
Dim tc As TabControl
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Function SetProperty(Of T)(ByRef storage As T, value As T, PropertyName As String) As Boolean
If Object.Equals(storage, value) Then Return False
storage = value
NotifyPropertyChanged(PropertyName)
Return True
End Function
Public Sub NotifyPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(propertyName))
End Sub
Private _IsChecked As Boolean
Public Property IsChecked As Boolean
Get
Return _IsChecked
End Get
Set(value As Boolean)
SetProperty(_IsChecked, value, "IsChecked")
End Set
End Property
Private _SelectedIndex As Integer
Public Property SelectedIndex As Integer
Get
Return _SelectedIndex
End Get
Set(value As Integer)
SetProperty(_SelectedIndex, value, "SelectedIndex")
End Set
End Property
Private Sub TabControl_Initialized(sender As Object, e As EventArgs)
tc = DirectCast(sender, TabControl)
AddHandler tc.Items.CurrentChanging, AddressOf Items_CurrentChanging
End Sub
Private Sub Items_CurrentChanging(sender As Object, e As CurrentChangingEventArgs)
Dim Result As MessageBoxResult
If SelectedIndex = 0 And IsChecked Then
Result = MessageBox.Show("Do you want to leave this tab?", "Leave", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No, MessageBoxOptions.ServiceNotification)
If Result = MessageBoxResult.No Then
e.Cancel = True
tc.SelectedItem = DirectCast(sender, ICollectionView).CurrentItem
End If
End If
End Sub
End Class
If you run this app and follow these steps you will find the tab control stops responding to tab changes.
1. Click TAB 2 (we move to TAB 2)
2. Check the Locked checkbox
3. Click TAB 1
4. Respond NO to the popup (we stay on TAB 2)
5. Click TAB 1 again
6. Response YES to the popup (we return to TAB 1)
7. Click TAB 2 again (we move to TAB 2)
8. Click on TAB 1
9. Response NO to the popup (we stay on TAB 2)
10. Click on TAB 1 again -- nothing happens!
NB: If you clear and check the Locked checkbox then functionality returns.
If you put a breakpoint in Items_CurrentChanging everything works correctly. Surely this is a threading issue - can anyone tell me what is wrong and, more importantly, how to fix it?
Thanks
It looks like the Items_CurrentChanging event is not being raised because the tab I'm clicking on still has focus (even though it is not the current item). I don't know why we have to go through two iterations to see the problem. The solution is to add a line of code after we set SelectedIndex. This sets focus back to the tab we didn't move away from.
Dispatcher.BeginInvoke(Sub() DirectCast(tc.SelectedItem, UIElement).Focus())
If anyone can shine a light on what's going on here, I'd be very grateful.

WPF INotifyPropertyChanged not updating label

I'm currently learning some basics in WPF and I've been looking for the mistake for about 2 days. Hope you guys can help.
I'm trying to update my UI (in this case the content of a label) by using INotifyPropertyChanged and a binding in XAML. The thing is: it only takes the first value and puts it in the content. Furthermore nothing happens but the event (OnPropertyChanged) is fired.
This is what I have in XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1" x:Class="MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:View x:Key="ViewModel"/>
</Window.Resources>
<Grid Margin="0,0,2,-4" DataContext="{Binding Source={StaticResource ViewModel}}">
....
<Label x:Name="lbl_money" Grid.ColumnSpan="2" Content="{Binding Path=PropMoney}" HorizontalAlignment="Left" Margin="403,42,0,0" VerticalAlignment="Top">
And this is the necessary part of my class View:
Public Class View
Inherits ViewModelBase
Private rest1 As New Restaurant
Private mainPlayer As New Player
Private mycurrentMoney As Double = 3
Private currentClickIncrease = mainPlayer.PropClickIncrease
Public Property PropMoney() As Double
Get
Return mycurrentMoney
End Get
Set(value As Double)
mycurrentMoney = value
OnPropertyChanged("mycurrentMoney")
End Set
End Property
Sub SelfClicked()
PropMoney() += 1
End Sub
Last but not least the MainWindow class, where i instantiate my view:
Class MainWindow
Private view As New View
Sub New()
InitializeComponent()
End Sub
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
view.SelfClicked()
End Sub
End Class
So my mycurrentMoney is increasing each click and the event is fired but the label doesn't update.
Thank you in advance!
If you have Visual Studio 15 use NameOf operator instead of string literal like so:
NameOf(PropMoney);
If you later rename your property, it will still work opposed to string literal which will NOT. Alternatively modify your OnPropertyChange to make use of CallerMemberName
OnPropertyChange ([System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
{
}
The property name will be filled in, this works only in setter for current property however.
Also, set DataContext for whole window (Setting DataContext in XAML in WPF). DataContext={StaticResource ViewModel} and don't use Path in your Binding, just {Binding PropertyName}
Your OnPropertyChanged("mycurrentMoney") statement won't raise a property change on your property, because it's called PropMoney.
You have to set OnPropertyChanged("PropMoney") in your setter instead.
There are 2 problems with your code
First you raise PropertyChanged event for the backing field and should raise it for property name
OnPropertyChanged("PropMoney")
Second the property you change belong to different instance of View then the one set as DataContext. So in XAML remove DataContext changes, only leave property binding
<Window ...>
<Grid Margin="0,0,2,-4">
<!-- .... -->
<Label ... Content="{Binding Path=PropMoney}">
and then in code set DataContext of MainWindow to the instance that you create and modify
Class MainWindow
Private view As New View
Sub New()
InitializeComponent()
DataContext = view
End Sub
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
view.SelfClicked()
End Sub
End Class

WPF binded Listbox bug when display 2 items

I have a WPF listbox in my window. In the Load event of the window, i create a List(of Object) and I added some items. At application starts or debug, I can see items.
If I add 1 item on the list, i correctly see 1 only item. If I add 3 or more items, i correctly see 3 or more items. If I add 2 only items, i see 1 only item. Why?
Here is my WPF code
<Window x:Class="Cacatua.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Cacatua;assembly=" >
<Grid>
<ListBox Name="lbSearch" ItemsSource="{Binding}" />
</Grid>
</Window>
And here is my code-behind (same assembly, in Cacatua namespace):
Private myLstSearch As List(Of Object)
Private Sub Window_Loaded(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
myLstSearch = New List(Of Object)
lbSearch.ItemsSource = myLstSearch
Dim myMedia1 as Media1
myMedia1 = New Media1("IdMedia1-A")
myLstSearch.Add(myMedia1)
myMedia1 = New Media1("IdMedia1-B")
myLstSearch.Add(myMedia1)
End Sub
where Media1 is a simple class that contains a string
Public Class Media1
Private myIdTitolo As String
Public ReadOnly Property IDTitolo As String
Get
Return (myIdTitolo)
End Get
End Property
Public Sub New(str As String)
myIdTitolo = str
End Sub
End Class
With this code, I would see a list with this output (there is no datatemplate):
Cacatua.Media1
Cacatua.Media1
but I see only
Cacatua.Media1
I think it's a bug. But am I the first with this problem?
You've got the right idea, but the problem is your ItemsSource doesn't know when to update since you're not using an ObservableCollection. Also there is a timing issue between rendering and loading the window, and I think this has to do with the fact you aren't properly binding your items source.
For starters, try changing the type of myLstSearch to ObservableCollection(Of Media1).
Also, a better way to do this would be to databind it from the XAML directly, so your code-behind would be something like:
Public property MyListSearch As ObservableCollection(Of Media1)
Then your XAML would look like:
<Window x:Class="Cacatua.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Cacatua;assembly=">
<Grid>
<ListBox Name="lbSearch" ItemsSource="{Binding Path=MyListSearch}" />
</Grid>
</Window>
That way, you can simply initialize MyListSearch in your window constructor, and then add elements to it whenever, while your view will automatically update.

Event firing when ComboBox.Items count changed?

I couldn't find a proper event which fires when my ComboBox.Items count changed. Is there any way to do so?
Bind ComboBox ItemsSource to ObservableCollection, then you can catch the event CollectionChanged of ObservableCollection
EDIT:
In wpf it is recommended to use binding instead of accessing UI element properties directly, of course better to use MVVM, but you can live without it too
in your Windows or UserControls C# code you can keep property like this
public ObservableCollection<string> MyCollection{get;set;}
Initialize it in constructor
MyCollection = new ObservableCollection<string>()
MyCollection.CollectionChanged += SomeMethod;
than name your UserControl in xaml like this
<UserControl Name="myUserControl".../>
write your ComboBox like this
<ComboBox ItemsSource="{Binding ElementName=myUserControl, Path=MyCollection}"...
now instead of adding and removing items to combobox element, add tham to MyCollection, they will appear in combobox
Hope this helps
Don't think that there is any event to fire when ComboBox.Items count changed. You probably should do the code when you add or remove the items.
Private Sub ComboBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox1.SelectedIndexChanged
End Sub
OR
protected void ComboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
}

Setting User Control's DataContext from Code-Behind

This should be pretty easy, but it throws VS2008 for a serious loop.
I'm trying out WPF with MVVM, and am a total newbie at it although I've been developing for about 15 years, and have a comp. sci. degree. At the current client, I am required to use VB.Net.
I have renamed my own variables and removed some distractions in the code below, so please forgive me if it's not 100% syntactically perfect! You probably don't really need the code to understand the question, but I'm including it in case it helps.
I have a very simple MainView.xaml file:
<Window x:Class="MyApp.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Main Window" Height="400" Width="800" Name="MainWindow">
<Button Name="Button1">Show Grid</Button>
<StackPanel Name="teststack" Visibility="Hidden"/>
</Window>
I also have a UserControl called DataView that consists of a DataGrid:
<UserControl x:Class="MyApp.Views.DataView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfToolkit="http://schemas.microsoft.com/wpf/2008/toolkit" >
<Grid>
<WpfToolkit:DataGrid
ItemsSource="{Binding Path=Entries}" SelectionMode="Extended">
</WpfToolkit:DataGrid>
</Grid>
</UserControl>
The constructor for the DataView usercontrol sets up the DataContext by binding it to a view model, as shown here:
Partial Public Class DataView
Dim dataViewModel As ViewModels.DataViewModel
Public Sub New()
InitializeComponent()
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
DataContext = dataViewModel
End Sub
End Class
The view model for DataView looks like this (there isn't much in ViewModelBase):
Public Class DataViewModel
Inherits ViewModelBase
Public Sub New()
End Sub
Private _entries As ObservableCollection(Of DataEntryViewModel) = New ObservableCollection(Of DataEntryViewModel)
Public ReadOnly Property Entries() As ObservableCollection(Of DataEntryViewModel)
Get
Return _entries
End Get
End Property
Public Sub LoadDataEntries()
Dim dataEntryList As List(Of DataEntry) = DataEntry.LoadDataEntries()
For Each dataentry As Models.DataEntry In dataEntryList
_entries.Add(New DataEntryViewModel(dataentry))
Next
End Sub
End Class
Now, this UserControl works just fine if I instantiate it in XAML. When I run the code, the grid shows up and populates it just fine.
However, the grid takes a long time to load its data, and I want to create this user control programmatically after the button click rather than declaratively instantiating the grid in XAML. I want to instantiate the user control, and insert it as a child of the StackPanel control:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim dataView As New DataView
teststack.Children.Add(dataView)
End Sub
When I do this, as soon as the Button1_Click finishes, my application locks up, starts eating RAM, and hits the CPU about 50%.
Am I not instantiating my UserControl properly? It all seems to come down to the DataContext assignment in DataEntry's constructor. If I comment that out, the app works as expected (without anything in the grid, of course).
If I move this code block into Button1_Click (basically moving DataEntry's constructor code up a level), the app still fails:
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
dataView.DataContext = dataViewModel
I'm stumped. Can anybody give me some tips on what I could be doing wrong, or even how to debug what infinite loop my app is getting itself into?
Many thanks.
The root cause of your issue appears to be either the raw amount of data you're loading or some inefficiency in how you load that data. Having said that, the reason you're seeing the application lock up is that you're locking the UI thread when loading the data.
I believe that in your first case the data loading has been off loaded onto another thread to load the data. In you second example you're instantiating the control on the UI thread and as a result all the constructor and loading logic is performed on the current thread (the UI thread). If you offload this work onto another thread then you should see similar results to the first example.
I eventually gave up on trying to get the DataContext on the UserControl set during instantiation of the UserControl (either in XAML or code). Now I load up the data and set the DataContext of the UserControl in an event in the UserControl (IsVisibleChanged, I believe). When I instantiate the UserControl in XAML, I have it's Visibility set to Hidden. When Button1 is clicked, I set the UserControl's Visibility to Visible. So the UserControl pops into view, and it loads up its data and DataContext is set. Seems to work, but also seems very kludgey. :-( Thanks for the help, folks!
If it's only a matter of your control taking a long time to populate data, you should populate the control on another thread then add it through a delegate:
Since I'm not too good at writing VB.NET, but here's the C# equivalent:
private void Button1_Click(Object sender, RoutedEventArgs e)
{
Thread thr = new Thread(delegate()
{
DataView dataView = new DataView();
this.Dispatcher.BeginInvoke((Action) delegate()
{
teststack.Children.Add(dataView);
});
});
thr.Start();
}

Resources