Related
I am facing an issue with ToolTips (again!)..
My code is as follows:
Xaml file:
<Grid>
<Button Height="23" Margin="82,0,120,105" Name="button1" VerticalAlignment="Bottom" ToolTip="{Binding Path=Label, Mode=Default}">Button</Button>
</Grid>
cs file:
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
Parameter p1;
System.Timers.Timer aTimer;
public Window1()
{
InitializeComponent();
p1 = new Parameter();
p1.Label = "One thing";
this.DataContext = p1;
aTimer = new System.Timers.Timer();
aTimer.Elapsed += new System.Timers.ElapsedEventHandler(aTimer_Elapsed);
aTimer.Interval = 5000;
aTimer.Enabled = true;
}
void aTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
aTimer.Enabled = false;
p1.Label = null;
}
}
The Parameter class is as follows:
class Parameter : System.ComponentModel.INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
#endregion
private string label = String.Empty;
public string Label
{
get { return label; }
set
{
label = value;
OnPropertyChanged(new PropertyChangedEventArgs("Label"));
}
}
private void OnPropertyChanged(PropertyChangedEventArgs propertyChangedEventArgs)
{
try
{
if (PropertyChanged != null)
{
PropertyChanged(this, propertyChangedEventArgs);
}
}
catch (Exception exc)
{
}
}
}
Now after the button is clicked, I get the tooltip "One thing" but after 5 seconds, I get an empty tooltip for sometime. Since I set the dependency property to null, I had expected no tooltip.
Afterwards, if I hover the mouse over the button, I get no empty ToolTip (as expected). Its only during the change of value I get the empty ToolTip.
Can you please help.
Try this... change your Label property and _label variable to type object instead of string i.e. use it as string but declare it as an object.
I guess the problem above happens because null string is actually string.Empty and ToolTip is of object type. The boxing that happens assumes null string as string.empty and hence shows a empty tooltip instead of no tooltip. But if Label is object tyupe then null value will represent no ToolTip.
If the suggested data type change of the Label property is not allowed in your case then then use a Converter in the Label binding to return null object for empty string.
Let me know if this helps.
Scenario: In a Silverlight 4 MVVM project, we have a ListBox control containing items, the selected item is two-way-bound to the appropriate property in the ViewModel. Another control (for example reasons, I've stripped it down to a single TextBox) is data bound to the selected item's content. The value should update on leave/focus lost.
Problem: When the value in the TextBox is changed and we leave that TextBox by pressing the Tab key, everything works as desired - the value is updated. However, if the user clicks on a different item in the ListBox, then the SelectedItem setter is fired before the content of TextBox setter is fired, leaving no chance to handle the user input.
You can see in debugger, when adding breakpoints to the property setters, that the new ListView selection is applied first, before the TextBox update is processed.
Desired behavior: We need to know that the currently selected item was modified before the user has selected another item. It's not desired to have a custom update trigger which would notify on each key press (we know that's possible).
Can you help?
Code (a very simple example):
ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ItemViewModel : ViewModelBase
{
private string _content;
public ItemViewModel(string initContent)
{
_content = initContent;
}
public string Content
{
get
{
return _content;
}
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
}
public ItemViewModel SelectedItem
{
get
{
return _selectedViewModel;
}
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
XAML
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Height="100"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
DisplayMemberPath="Content"
Width="220" />
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="12,118,0,0"
Text="{Binding SelectedItem.Content, Mode=TwoWay}"
VerticalAlignment="Top"
Width="220" />
</Grid>
XAML Code Behind
public MvvmTestView()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
}
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
MainViewModel viewModel = new MainViewModel();
viewModel.Items.Add(new ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new ItemViewModel("Thanks to Community"));
DataContext = viewModel;
}
UPDATE 1
I present a self designed solution for you to check out, which will be probably be the accepted one, I still want to encourage you to make comments and give your hints. Thanks.
You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.
Here´s the code for the Behavior class:
public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
// Fields
private BindingExpression expression;
// Methods
protected override void OnAttached() {
base.OnAttached();
this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
base.AssociatedObject.TextChanged+= OnTextChanged;
}
protected override void OnDetaching() {
base.OnDetaching();
base.AssociatedObject.TextChanged-= OnTextChanged;
this.expression = null;
}
private void OnTextChanged(object sender, EventArgs args) {
this.expression.UpdateSource();
}
}
Heres the XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"
<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox >
This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).
How to get there?
In ViewModelBase, implement a new ForceBindingUpdate event:
public abstract class ViewModelBase : INotifyPropertyChanged
{
// ----- leave everything from original code ------
public event EventHandler ForceBindingUpdate;
protected void OnForceBindingUpdate()
{
var handler = ForceBindingUpdate;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
In MainViewModel, update the setter of the SelectedItem property:
set // of SelectedItem Property
{
if (_selectedViewModel != value)
{
// Ensure Data Update - the new part
OnForceBindingUpdate();
// Old stuff
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
Update the MvvmTestView Code Behind to implement the new event:
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
// remains unchanged
Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));
// Ensure Data Update by rebinding the content property - the new part
viewModel.ForceBindingUpdate += (s, a) =>
{
var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();
};
// remains unchanged
DataContext = viewModel;
}
Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.
Done.
Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.
Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?
Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.
Solution №1
public class LazyTextBox: TextBox
{
//bind to that property instead..
public string LazyText
{
get { return (string)GetValue(LazyTextProperty); }
set { SetValue(LazyTextProperty, value); }
}
public static readonly DependencyProperty LazyTextProperty =
DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox),
new PropertyMetadata(null));
//call this method when it's really nessasary...
public void EnsureThatLazyTextEqualText()
{
if (this.Text != this.LazyText)
{
this.LazyText = this.Text;
}
}
}
Solution №2 (works as magic :) )
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items { get { return _items; } }
public ItemViewModel SelectedItem
{
get { return _selectedViewModel; }
set
{
if (_selectedViewModel != value)
{
if (SelectedItem != null)
{
SelectedItem.Content = SelectedItem.Content;
}
_selectedViewModel = value;
// A little delay make no harm :)
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0.1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
}
}
void t_Tick(object sender, EventArgs e)
{
OnPropertyChanged("SelectedItem");
(sender as DispatcherTimer).Stop();
}
}
I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.
By putting a ghost element to take focus we can swap the focus back in forth forcing
the text box to commit its contents. So in the code behind we take care of the focus wiggle.
But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.
<MenuItem Header="_Save"
Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
UIElement elem = Keyboard.FocusedElement as UIElement;
Keyboard.Focus(ghost);
Keyboard.Focus(elem);
}
Solution #3
public abstract class ViewModelBase : INotifyPropertyChanged
{
private List<string> _propNameList = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
_propNameList.Add(propertyName);
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
void t_Tick(object sender, EventArgs e)
{
if (_propNameList.Count > 0)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(_propNameList[0]));
_propNameList.Remove(_propNameList[0]);
}
}
}
PS: it's the same timer.. but this solution is more generic..
I'm using Silverlight on Windows Phone 7.
I want to display the first part of some text in a TextBlock in bold, and the rest in normal font. The complete text must wrap. I want the bolded part to contain text from one property in my ViewModel, and the plain text to contain text from a different property.
The TextBlock is defined in a DataTemplate associated with a LongListSelector.
My initial attempt was:
<TextBlock TextWrapping="Wrap">
<TextBlock.Inlines>
<Run Text="{Binding Property1}" FontWeight="Bold"/>
<Run Text="{Binding Property2}"/>
</TextBlock.Inlines>
</TextBlock>
This fails at runtime with the spectacularly unhelpful "AG_E_RUNTIME_MANAGED_UNKNOWN_ERROR". This is a known issue because the Run element is not a FrameworkElement and cannot be bound.
My next attempt was to put placeholders in place, and then update them in code:
<TextBlock Loaded="TextBlockLoaded" TextWrapping="Wrap">
<TextBlock.Inlines>
<Run FontWeight="Bold">Placeholder1</Run>
<Run>Placeholder2</Run>
</TextBlock.Inlines>
</TextBlock>
In the code-behind (yes I am desparate!):
private void TextBlockLoaded(object sender, RoutedEventArgs e)
{
var textBlock = (TextBlock)sender;
var viewModel = (ViewModel)textBlock.DataContext;
var prop1Run = (Run)textBlock.Inlines[0];
var prop2Run = (Run)textBlock.Inlines[1];
prop1Run.Text = viewModel.Property1;
prop2Run.Text = viewModel.Property2;
}
This seemed to work, but because I am using the LongListSelector, although items get recycled, the Loaded codebehind event handler doesn't re-initialize the Runs, so very quickly the wrong text is displayed...
I've looked at using the LongListSelector's Linked event (which I already use to free up images that I display in the list), but I can't see how I can use that to re-initialize the Runs' text properties.
Any help appreciated!
I finally found a solution that works for me.
As I mention in the comment, Paul Stovell's approach would not work.
Instead I used a similar approach to add an attached property to the TextBlock, bound to the TextBlock's DataContext, and attached properties on the runs, indicating which ViewModel properties they should be bound to:
<TextBlock TextWrapping="Wrap"
Views:BindableRuns.Target="{Binding}">
<TextBlock.Inlines>
<Run FontWeight="Bold" Views:BindableRuns.Target="Property1"/>
<Run Views:BindableRuns.Target="Property2"/>
</TextBlock.Inlines>
</TextBlock>
Then in my attached TextBox Target (datacontext) property's changed event, I update the Runs, and subscribe to be notified of changes to the TextBox Target properties. When a TextBox Target property changes, I updated any associated Run's text accordingly.
public static class BindableRuns
{
private static readonly Dictionary<INotifyPropertyChanged, PropertyChangedHandler>
Handlers = new Dictionary<INotifyPropertyChanged, PropertyChangedHandler>();
private static void TargetPropertyPropertyChanged(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
if(!(dependencyObject is TextBlock)) return;
var textBlock = (TextBlock)dependencyObject;
AddHandler(e.NewValue as INotifyPropertyChanged, textBlock);
RemoveHandler(e.OldValue as INotifyPropertyChanged);
InitializeRuns(textBlock, e.NewValue);
}
private static void AddHandler(INotifyPropertyChanged dataContext,
TextBlock textBlock)
{
if (dataContext == null) return;
var propertyChangedHandler = new PropertyChangedHandler(textBlock);
dataContext.PropertyChanged += propertyChangedHandler.PropertyChanged;
Handlers[dataContext] = propertyChangedHandler;
}
private static void RemoveHandler(INotifyPropertyChanged dataContext)
{
if (dataContext == null || !Handlers.ContainsKey(dataContext)) return;
dataContext.PropertyChanged -= Handlers[dataContext].PropertyChanged;
Handlers.Remove(dataContext);
}
private static void InitializeRuns(TextBlock textBlock, object dataContext)
{
if (dataContext == null) return;
var runs = from run in textBlock.Inlines.OfType<Run>()
let propertyName = (string)run.GetValue(TargetProperty)
where propertyName != null
select new { Run = run, PropertyName = propertyName };
foreach (var run in runs)
{
var property = dataContext.GetType().GetProperty(run.PropertyName);
run.Run.Text = (string)property.GetValue(dataContext, null);
}
}
private class PropertyChangedHandler
{
private readonly TextBlock _textBlock;
public PropertyChangedHandler(TextBlock textBlock)
{
_textBlock = textBlock;
}
public void PropertyChanged(object sender,
PropertyChangedEventArgs propertyChangedArgs)
{
var propertyName = propertyChangedArgs.PropertyName;
var run = _textBlock.Inlines.OfType<Run>()
.Where(r => (string) r.GetValue(TargetProperty) == propertyName)
.SingleOrDefault();
if(run == null) return;
var property = sender.GetType().GetProperty(propertyName);
run.Text = (string)property.GetValue(sender, null);
}
}
public static object GetTarget(DependencyObject obj)
{
return obj.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject obj,
object value)
{
obj.SetValue(TargetProperty, value);
}
public static readonly DependencyProperty TargetProperty =
DependencyProperty.RegisterAttached("Target",
typeof(object),
typeof(BindableRuns),
new PropertyMetadata(null,
TargetPropertyPropertyChanged));
}
I suggest you give the BindableRun a try. I've only used it in WPF, but I don't see why it wouldn't work in Silverlight.
I'm new to WPF, and I'm trying to do what I thought would be a simple task - display the value of a field in my business object as it changes during my program. I know how to "force" it to change by manually changing the TextBox.Text property in C#, but as I'm learning WPF I want to do it the "right" way, and that means databinding.
Question #1: As far as I understand it, my choice is to either use a DependencyProperty or implement INotifyPropertyChanged in my business object, right?
Question #2: Here is a generic version of my code in which I attempted to go the DependencyProperty route.
Markup:
Button x:Name="nextButton" Content="Click" Grid.Row="2" Click="nextButton_Click" />
TextBox x:Name="myTextBox" Grid.Row="1" Text="{Binding Source=myTest, Path=Name, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}"/>
Code-Behind:
namespace DependencyTest2
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
private int i;
private TestSphere myTest;
public MainWindow()
{
InitializeComponent();
i = 0;
myTest = new TestSphere();
}
private void nextButton_Click(object sender, RoutedEventArgs e)
{
switch (i)
{
case 0:
myTest.Name = "string1";
break;
case 1:
myTest.Name = "string2";
break;
case 2:
myTest.Name = "string3";
break;
}
i++;
}
}
class TestSphere : DependencyObject
{
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(TestSphere));
public TestSphere()
{
Name = "default";
}
}
}
When I run the program, nothing appears in text box, even though the bound property has a value - is there something else I need to do to alert the binding that the source value has changed? I thought that using a DependencyProperty as the source would take care of that, but then again, I'm a WPF rookie. Thanks!
Steve
Ok, I tried to implement INotifyPropertyChanged using a wrapper class I found on codeproject as follows:
class TestSphere : NotifyProperyChangedBase
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
this.CheckPropertyChanged("Name", ref _Name, ref value);
}
}
public TestSphere()
{
Name = "default";
}
}
public abstract class NotifyProperyChangedBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region methods
protected bool CheckPropertyChanged(string propertyName, ref T oldValue, ref T newValue)
{
if (oldValue == null && newValue == null)
{
return false;
}
if ((oldValue == null && newValue != null) || !oldValue.Equals((T)newValue))
{
oldValue = newValue;
FirePropertyChanged(propertyName);
return true;
}
return false;
}
protected void FirePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
Here is my new Markup, too:
Grid Name="myGrid">
Grid.RowDefinitions>
RowDefinition Height="30"/>
RowDefinition Height="30"/>
RowDefinition Height="30"/>
RowDefinition Height="*"/>
/Grid.RowDefinitions>
Label x:Name="myLabel" Grid.Row="0" Foreground="Black" />
Button x:Name="nextButton" Content="Click" Grid.Row="2" Click="nextButton_Click" />
TextBox x:Name="myTextBox" Grid.Row="1" Text="{Binding Path=myTest.Name}"/>
/Grid>
I also added the line myGrid.DataContext = myTest; to 'public MainWindow()' immediately after I instantiate myTest. When I step through the resulting program, the value of this.PropertyChanged always evaluates to null, so that the PropertyChanged even never fires. Sorry in advance for what must be a really noob question.
Steve
You should only need to implement INotifyPropertyChanged on the TestSphere class, not DependencyObject. As you update the value, call PropertyChanged(this, new PropertyChangedEventArgs("Name")).
Second, you need to set the DataContext for the window in your code-behind. Lets say you used this in your XAML for the root grid element:
<Grid Name="MainForm">
Then in your code-behind, you'd do this:
MainForm.DataContext = this;
Finally, change the myTest property to public, and the binding in your XAML should then only need to be
Text="{Binding Path=myTest.Name}"
I've got a WPF application with a Treeview control.
When the user clicks a node on the tree, other TextBox, ComboBox, etc. controls on the page are populated with appropriate values.
The user can then make changes to those values and save his or her changes by clicking a Save button.
However, if the user selects a different Treeview node without saving his or her changes, I want to display a warning and an opportunity to cancel that selection.
MessageBox: Continue and discard your unsaved changes? OK/Cancel http://img522.imageshack.us/img522/2897/discardsj3.gif
XAML...
<TreeView Name="TreeViewThings"
...
TreeViewItem.Unselected="TreeViewThings_Unselected"
TreeViewItem.Selected="TreeViewThings_Selected" >
Visual Basic...
Sub TreeViewThings_Unselected(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim OldThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
If CancelDueToUnsavedChanges(OldThing) Then
'put canceling code here
End If
End Sub
Sub TreeViewThings_Selected(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim NewThing As Thing = DirectCast(e.OriginalSource.DataContext, Thing)
PopulateControlsFromThing(NewThing)
End Sub
How can I cancel those unselect/select events?
Update: I've asked a follow-up question...
How do I properly handle a PreviewMouseDown event with a MessageBox confirmation?
UPDATE
Realized I could put the logic in SelectedItemChanged instead. A little cleaner solution.
Xaml
<TreeView Name="c_treeView"
SelectedItemChanged="c_treeView_SelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Code behind. I have some classes that is my ItemsSource of the TreeView so I made an interface (MyInterface) that exposes the IsSelected property for all of them.
private MyInterface m_selectedTreeViewItem = null;
private void c_treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (m_selectedTreeViewItem != null)
{
if (e.NewValue == m_selectedTreeViewItem)
{
// Will only end up here when reversing item
// Without this line childs can't be selected
// twice if "No" was pressed in the question..
c_treeView.Focus();
}
else
{
if (MessageBox.Show("Change TreeViewItem?",
"Really change",
MessageBoxButton.YesNo,
MessageBoxImage.Question) != MessageBoxResult.Yes)
{
EventHandler eventHandler = null;
eventHandler = new EventHandler(delegate
{
c_treeView.LayoutUpdated -= eventHandler;
m_selectedTreeViewItem.IsSelected = true;
});
// Will be fired after SelectedItemChanged, to early to change back here
c_treeView.LayoutUpdated += eventHandler;
}
else
{
m_selectedTreeViewItem = e.NewValue as MyInterface;
}
}
}
else
{
m_selectedTreeViewItem = e.NewValue as MyInterface;
}
}
I haven't found any situation where it doesn't revert back to the previous item upon pressing "No".
I had to solve the same problem, but in multiple treeviews in my application. I derived TreeView and added event handlers, partly using Meleak's solution and partly using the extension methods from this forum: http://forums.silverlight.net/t/65277.aspx/1/10
I thought I'd share my solution with you, so here is my complete reusable TreeView that handles "cancel node change":
public class MyTreeView : TreeView
{
public static RoutedEvent PreviewSelectedItemChangedEvent;
public static RoutedEvent SelectionCancelledEvent;
static MyTreeView()
{
PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent("PreviewSelectedItemChanged", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<object>), typeof(MyTreeView));
SelectionCancelledEvent = EventManager.RegisterRoutedEvent("SelectionCancelled", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(MyTreeView));
}
public event RoutedPropertyChangedEventHandler<object> PreviewSelectedItemChanged
{
add { AddHandler(PreviewSelectedItemChangedEvent, value); }
remove { RemoveHandler(PreviewSelectedItemChangedEvent, value); }
}
public event RoutedEventHandler SelectionCancelled
{
add { AddHandler(SelectionCancelledEvent, value); }
remove { RemoveHandler(SelectionCancelledEvent, value); }
}
private object selectedItem = null;
protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue == selectedItem)
{
this.Focus();
var args = new RoutedEventArgs(SelectionCancelledEvent);
RaiseEvent(args);
}
else
{
var args = new RoutedPropertyChangedEventArgs<object>(e.OldValue, e.NewValue, PreviewSelectedItemChangedEvent);
RaiseEvent(args);
if (args.Handled)
{
EventHandler eventHandler = null;
eventHandler = delegate
{
this.LayoutUpdated -= eventHandler;
var treeViewItem = this.ContainerFromItem(selectedItem);
if (treeViewItem != null)
treeViewItem.IsSelected = true;
};
this.LayoutUpdated += eventHandler;
}
else
{
selectedItem = this.SelectedItem;
base.OnSelectedItemChanged(e);
}
}
}
}
public static class TreeViewExtensions
{
public static TreeViewItem ContainerFromItem(this TreeView treeView, object item)
{
if (item == null) return null;
var containerThatMightContainItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(item);
return containerThatMightContainItem ?? ContainerFromItem(treeView.ItemContainerGenerator, treeView.Items, item);
}
private static TreeViewItem ContainerFromItem(ItemContainerGenerator parentItemContainerGenerator, ItemCollection itemCollection, object item)
{
foreach (var child in itemCollection)
{
var parentContainer = (TreeViewItem)parentItemContainerGenerator.ContainerFromItem(child);
var containerThatMightContainItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
if (containerThatMightContainItem != null)
return containerThatMightContainItem;
var recursionResult = ContainerFromItem(parentContainer.ItemContainerGenerator, parentContainer.Items, item);
if (recursionResult != null)
return recursionResult;
}
return null;
}
}
Here is an example of usage (codebehind for window containing a MyTreeView):
private void theTreeView_PreviewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.OldValue != null)
e.Handled = true;
}
private void theTreeView_SelectionCancelled(object sender, RoutedEventArgs e)
{
MessageBox.Show("Cancelled");
}
After choosing the first node in the treeview, all other node changes are cancelled and a message box is displayed.
You can't actually put your logic into the OnSelectedItemChanged Method, if the logic is there the Selected Item has actually already changed.
As suggested by another poster, the PreviewMouseDown handler is a better spot to implement the logic, however, a fair amount of leg work still needs to be done.
Below is my 2 cents:
First the TreeView that I have implemented:
public class MyTreeView : TreeView
{
static MyTreeView( )
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MyTreeView),
new FrameworkPropertyMetadata(typeof(TreeView)));
}
// Register a routed event, note this event uses RoutingStrategy.Tunnel. per msdn docs
// all "Preview" events should use tunneling.
// http://msdn.microsoft.com/en-us/library/system.windows.routedevent.routingstrategy.aspx
public static RoutedEvent PreviewSelectedItemChangedEvent = EventManager.RegisterRoutedEvent(
"PreviewSelectedItemChanged",
RoutingStrategy.Tunnel,
typeof(CancelEventHandler),
typeof(MyTreeView));
// give CLR access to routed event
public event CancelEventHandler PreviewSelectedItemChanged
{
add
{
AddHandler(PreviewSelectedItemChangedEvent, value);
}
remove
{
RemoveHandler(PreviewSelectedItemChangedEvent, value);
}
}
// override PreviewMouseDown
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
// determine which item is going to be selected based on the current mouse position
object itemToBeSelected = this.GetObjectAtPoint<TreeViewItem>(e.GetPosition(this));
// selection doesn't change if the target point is null (beyond the end of the list)
// or if the item to be selected is already selected.
if (itemToBeSelected != null && itemToBeSelected != SelectedItem)
{
bool shouldCancel;
// call our new event
OnPreviewSelectedItemChanged(out shouldCancel);
if (shouldCancel)
{
// if we are canceling the selection, mark this event has handled and don't
// propogate the event.
e.Handled = true;
return;
}
}
// otherwise we want to continue normally
base.OnPreviewMouseDown(e);
}
protected virtual void OnPreviewSelectedItemChanged(out bool shouldCancel)
{
CancelEventArgs e = new CancelEventArgs( );
if (PreviewSelectedItemChangedEvent != null)
{
// Raise our event with our custom CancelRoutedEventArgs
RaiseEvent(new CancelRoutedEventArgs(PreviewSelectedItemChangedEvent, e));
}
shouldCancel = e.Cancel;
}
}
some extension methods to support the TreeView finding the object under the mouse.
public static class ItemContainerExtensions
{
// get the object that exists in the container at the specified point.
public static object GetObjectAtPoint<ItemContainer>(this ItemsControl control, Point p)
where ItemContainer : DependencyObject
{
// ItemContainer - can be ListViewItem, or TreeViewItem and so on(depends on control)
ItemContainer obj = GetContainerAtPoint<ItemContainer>(control, p);
if (obj == null)
return null;
// it is worth noting that the passed _control_ may not be the direct parent of the
// container that exists at this point. This can be the case in a TreeView, where the
// parent of a TreeViewItem may be either the TreeView or a intermediate TreeViewItem
ItemsControl parentGenerator = obj.GetParentItemsControl( );
// hopefully this isn't possible?
if (parentGenerator == null)
return null;
return parentGenerator.ItemContainerGenerator.ItemFromContainer(obj);
}
// use the VisualTreeHelper to find the container at the specified point.
public static ItemContainer GetContainerAtPoint<ItemContainer>(this ItemsControl control, Point p)
where ItemContainer : DependencyObject
{
HitTestResult result = VisualTreeHelper.HitTest(control, p);
DependencyObject obj = result.VisualHit;
while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemContainer))
{
obj = VisualTreeHelper.GetParent(obj);
}
// Will return null if not found
return obj as ItemContainer;
}
// walk up the visual tree looking for the nearest ItemsControl parent of the specified
// depObject, returns null if one isn't found.
public static ItemsControl GetParentItemsControl(this DependencyObject depObject)
{
DependencyObject obj = VisualTreeHelper.GetParent(depObject);
while (VisualTreeHelper.GetParent(obj) != null && !(obj is ItemsControl))
{
obj = VisualTreeHelper.GetParent(obj);
}
// will return null if not found
return obj as ItemsControl;
}
}
and last, but not least the custom EventArgs that leverage the RoutedEvent subsystem.
public class CancelRoutedEventArgs : RoutedEventArgs
{
private readonly CancelEventArgs _CancelArgs;
public CancelRoutedEventArgs(RoutedEvent #event, CancelEventArgs cancelArgs)
: base(#event)
{
_CancelArgs = cancelArgs;
}
// override the InvokeEventHandler because we are going to pass it CancelEventArgs
// not the normal RoutedEventArgs
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
{
CancelEventHandler handler = (CancelEventHandler)genericHandler;
handler(genericTarget, _CancelArgs);
}
// the result
public bool Cancel
{
get
{
return _CancelArgs.Cancel;
}
}
}
Instead of selecting for Selected/Unselected, a better route might be to hook into PreviewMouseDown. The preblem with handling a Selected and Unselected event is that the event has already occurred when you receive the notification. There is nothing to cancel because it's already happened.
On the other hand, Preview events are cancelable. It's not the exact event you want but it does give you the oppuritunity to prevent the user from selecting a different node.
You can't cancel the event like you can, for example, a Closing event. But you can undo it if you cache the last selected value. The secret is you have to change the selection without re-firing the SelectionChanged event. Here's an example:
private object _LastSelection = null;
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (IsUpdated)
{
MessageBoxResult result = MessageBox.Show("The current record has been modified. Are you sure you want to navigate away? Click Cancel to continue editing. If you click OK all changes will be lost.", "Warning", MessageBoxButton.OKCancel, MessageBoxImage.Hand);
switch (result)
{
case MessageBoxResult.Cancel:
e.Handled = true;
// disable event so this doesn't go into an infinite loop when the selection is changed to the cached value
PersonListView.SelectionChanged -= new SelectionChangedEventHandler(OnSelectionChanged);
PersonListView.SelectedItem = _LastSelection;
PersonListView.SelectionChanged += new SelectionChangedEventHandler(OnSelectionChanged);
return;
case MessageBoxResult.OK:
// revert the object to the original state
LocalDataContext.Persons.GetOriginalEntityState(_LastSelection).CopyTo(_LastSelection);
IsUpdated = false;
Refresh();
break;
default:
throw new ApplicationException("Invalid response.");
}
}
// cache the selected item for undo
_LastSelection = PersonListView.SelectedItem;
}
CAMS_ARIES:
XAML:
code :
private bool ManejarSeleccionNodoArbol(Object origen)
{
return true; // with true, the selected nodo don't change
return false // with false, the selected nodo change
}
private void Arbol_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.Source is TreeViewItem)
{
e.Handled = ManejarSeleccionNodoArbol(e.Source);
}
}
private void Arbol_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Source is TreeViewItem)
{
e.Handled=ManejarSeleccionNodoArbol(e.Source);
}
}
Since the SelectedItemChanged event is triggered after the SelectedItem has already changed, you can't really cancel the event at this point.
What you can do is listen for mouse-clicks and cancel them before the SelectedItem gets changed.
You could create your custom control that derives from TreeView and then override the OnSelectedItemChanged method.
Before calling the base, you could first fire a custom event with a CancelEventArgs parameter. If the parameter.Cancel become true, then don't call the base, but select the old item instead (be careful that the OnSelectedItemChanged will be called again).
Not the best solution, but at least this keeps the logic inside the tree control, and there is not chance that the selection change event fires more than it's needed. Also, you don't need to care if the user clicked the tree, used the keyboard or maybe the selection changed programatically.
I solved this problem for 1 tree view and display of 1 document at a time. This solution is based on an attachable behavior that can be attached to a normal treeview:
<TreeView Grid.Column="0"
ItemsSource="{Binding TreeViewItems}"
behav:TreeViewSelectionChangedBehavior.ChangedCommand="{Binding SelectItemChangedCommand}"
>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
ToolTipService.ShowOnDisabled="True"
VerticalAlignment="Center" Margin="3" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
and the code for the behavior is this:
/// <summary>
/// Source:
/// http://stackoverflow.com/questions/1034374/drag-and-drop-in-mvvm-with-scatterview
/// http://social.msdn.microsoft.com/Forums/de-DE/wpf/thread/21bed380-c485-44fb-8741-f9245524d0ae
///
/// Attached behaviour to implement the SelectionChanged command/event via delegate command binding or routed commands.
/// </summary>
public static class TreeViewSelectionChangedBehavior
{
#region fields
/// <summary>
/// Field of attached ICommand property
/// </summary>
private static readonly DependencyProperty ChangedCommandProperty = DependencyProperty.RegisterAttached(
"ChangedCommand",
typeof(ICommand),
typeof(TreeViewSelectionChangedBehavior),
new PropertyMetadata(null, OnSelectionChangedCommandChange));
/// <summary>
/// Implement backing store for UndoSelection dependency proeprty to indicate whether selection should be
/// cancelled via MessageBox query or not.
/// </summary>
public static readonly DependencyProperty UndoSelectionProperty =
DependencyProperty.RegisterAttached("UndoSelection",
typeof(bool),
typeof(TreeViewSelectionChangedBehavior),
new PropertyMetadata(false, OnUndoSelectionChanged));
#endregion fields
#region methods
#region ICommand changed methods
/// <summary>
/// Setter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <param name="value"></param>
public static void SetChangedCommand(DependencyObject source, ICommand value)
{
source.SetValue(ChangedCommandProperty, value);
}
/// <summary>
/// Getter method of the attached ChangedCommand <seealso cref="ICommand"/> property
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static ICommand GetChangedCommand(DependencyObject source)
{
return (ICommand)source.GetValue(ChangedCommandProperty);
}
#endregion ICommand changed methods
#region UndoSelection methods
public static bool GetUndoSelection(DependencyObject obj)
{
return (bool)obj.GetValue(UndoSelectionProperty);
}
public static void SetUndoSelection(DependencyObject obj, bool value)
{
obj.SetValue(UndoSelectionProperty, value);
}
#endregion UndoSelection methods
/// <summary>
/// This method is hooked in the definition of the <seealso cref="ChangedCommandProperty"/>.
/// It is called whenever the attached property changes - in our case the event of binding
/// and unbinding the property to a sink is what we are looking for.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnSelectionChangedCommandChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeView uiElement = d as TreeView; // Remove the handler if it exist to avoid memory leaks
if (uiElement != null)
{
uiElement.SelectedItemChanged -= Selection_Changed;
var command = e.NewValue as ICommand;
if (command != null)
{
// the property is attached so we attach the Drop event handler
uiElement.SelectedItemChanged += Selection_Changed;
}
}
}
/// <summary>
/// This method is called when the selection changed event occurs. The sender should be the control
/// on which this behaviour is attached - so we convert the sender into a <seealso cref="UIElement"/>
/// and receive the Command through the <seealso cref="GetChangedCommand"/> getter listed above.
///
/// The <paramref name="e"/> parameter contains the standard EventArgs data,
/// which is unpacked and reales upon the bound command.
///
/// This implementation supports binding of delegate commands and routed commands.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void Selection_Changed(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var uiElement = sender as TreeView;
// Sanity check just in case this was somehow send by something else
if (uiElement == null)
return;
ICommand changedCommand = TreeViewSelectionChangedBehavior.GetChangedCommand(uiElement);
// There may not be a command bound to this after all
if (changedCommand == null)
return;
// Check whether this attached behaviour is bound to a RoutedCommand
if (changedCommand is RoutedCommand)
{
// Execute the routed command
(changedCommand as RoutedCommand).Execute(e.NewValue, uiElement);
}
else
{
// Execute the Command as bound delegate
changedCommand.Execute(e.NewValue);
}
}
/// <summary>
/// Executes when the bound boolean property indicates that a user should be asked
/// about changing a treeviewitem selection instead of just performing it.
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnUndoSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TreeView uiElement = d as TreeView; // Remove the handler if it exist to avoid memory leaks
if (uiElement != null)
{
uiElement.PreviewMouseDown -= uiElement_PreviewMouseDown;
var command = (bool)e.NewValue;
if (command == true)
{
// the property is attached so we attach the Drop event handler
uiElement.PreviewMouseDown += uiElement_PreviewMouseDown;
}
}
}
/// <summary>
/// Based on the solution proposed here:
/// Source: http://stackoverflow.com/questions/20244916/wpf-treeview-selection-change
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void uiElement_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
// first did the user click on a tree node?
var source = e.OriginalSource as DependencyObject;
while (source != null && !(source is TreeViewItem))
source = VisualTreeHelper.GetParent(source);
var itemSource = source as TreeViewItem;
if (itemSource == null)
return;
var treeView = sender as TreeView;
if (treeView == null)
return;
bool undoSelection = TreeViewSelectionChangedBehavior.GetUndoSelection(treeView);
if (undoSelection == false)
return;
// Cancel the attempt to select an item.
var result = MessageBox.Show("The current document has unsaved data. Do you want to continue without saving data?", "Are you really sure?",
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
if (result == MessageBoxResult.No)
{
// Cancel the attempt to select a differnet item.
e.Handled = true;
}
else
{
// Lets disable this for a moment, otherwise, we'll get into an event "recursion"
treeView.PreviewMouseDown -= uiElement_PreviewMouseDown;
// Select the new item - make sure a SelectedItemChanged event is fired in any case
// Even if this means that we have to deselect/select the one and the same item
if (itemSource.IsSelected == true )
itemSource.IsSelected = false;
itemSource.IsSelected = true;
// Lets enable this to get back to business for next selection
treeView.PreviewMouseDown += uiElement_PreviewMouseDown;
}
}
#endregion methods
}
In this example I am showing a blocking message box in order to block the PreviewMouseDown event when it occurs. The event is then handled to signal that selection is cancelled or it is not handled to let the treeview itself handle the event by selecting the item that is about to be selected.
The behavior then invokes a bound command in the viewmodel if the user decides to continue anyway (PreviewMouseDown event is not handled by attached behavior and bound command is invoked.
I guess the message box showing could be done in other ways but I think its essential here to block the event when it happens since its otherwise not possible to cancel it(?). So, the only improve I could possible think off about this code is to bind some strings to make the displayed message configurable.
I have written an article that contains a downloadable sample since this is otherwise a difficult area to explain (one has to make many assumptions about missing parts that and the may not always be shared by all readers)
Here is an article that contains my results:
http://www.codeproject.com/Articles/995629/Cancelable-TreeView-Navigation-for-Documents-in-WP
Please comment on this solution and let me know if you see room for improvement.