Change binding when focus changes - c#

I have an application that has several treeviews and one propertygrid (from the extended WPF toolkit). The goal is to display the properties of the selected item. I'm fairly new to WPF so I started with one treeview and bind the propertygrids selected object like this
<xctk:PropertyGrid x:Name="xctkPropertyGrid"
Grid.Column="2"
ShowSearchBox="False"
ShowSortOptions="False"
SelectedObject="{Binding ElementName=actionsTreeView, Path=SelectedItem, Mode=OneWay}">
</xctk:PropertyGrid>
This seems to work fine. But it off course binds to actionsTreeView all the time. What I would really need is an update of that propertygrid when the focus changes to another selecteditem in another treeview. I have achieved my goal using the SelectedItemChanged of each treeview and set the propertygrids selectedobject like so. Is this somehow possible using databinding and triggers. My solution adds some code behind and tight coupling and that doesn't feel very MVVM.
kind regards,
Jef

Ok, here's how I ended up solving my problem:
Each treeview is bound to a viemodel property on the main viewmodel. I also created a SelectedItem property on the main viewmodel like this to which the propertygrid's SelectedObject is bound:
private object selectedItem;
public object SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
I then attach a behavior to each treeview that updates this SelectedItem:
public class UpdateSelectedItemBehavior : Behavior<TreeView>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.GotFocus += AssociatedObject_GotFocus;
this.AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
}
void AssociatedObject_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
ViewModels.MainViewModel mainViewModel = AssociatedObject.DataContext as ViewModels.MainViewModel;
if (mainViewModel != null)
{
mainViewModel.SelectedItem = AssociatedObject.SelectedItem;
}
}
void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
{
ViewModels.MainViewModel mainViewModel = AssociatedObject.DataContext as ViewModels.MainViewModel;
if (mainViewModel != null)
{
mainViewModel.SelectedItem = AssociatedObject.SelectedItem;
}
}
}

Related

WPF MVVM - Update Datagrid of the MainView from a child viewmodel

I think it may be quite simple but I have been googling a solution for a while without success, and I am not sure what is the correct approach. I saw that Prism could be a solution but I am looking for something simple.
I have a MainViewModel and a MainView which contains a Datagrid. The MainView contains also a ContentControl which display a ChildView (with DataContext as ChildViewModel).
I use DataTemplate to associate Views and ViewModels.
The DataGrid of the MainView is Binded to an ObservableCollection of the ChildViewModel.
I want this DataGrid to be updated every time this Collection is modified.
I have tried to use the INotifyPropertyChanged.
I have tried to use the OnCollectionChanged.
When I debug I can see that the Collection has changed and that event is fired but how to refresh the binding ? (dataGrid is not updated).
Here is the code:
DataTemplate
<DataTemplate DataType="{x:Type vm:ChildViewModel}">
<vi:ChildView />
</DataTemplate>
XAML MainView
<ContentControl Content="{Binding childViewModel}"/>
<DataGrid x:Name="DataGridChildren"
ItemsSource="{Binding childViewModel.Children,Mode=TwoWay}"
SelectedItem="{Binding childViewModel.SelectedItem}" EnableRowVirtualization="True" IsReadOnly="True" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
MainViewModel
public ChildViewModel childViewModel { get; set; }
public MainViewModel()
{
childViewModel = new ChildViewModel();
}
ViewModelBase
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
handler?.Invoke(this, args);
}
}
ChildViewModel
public class ChildViewModel: ViewModelBase
{
private ObservableCollection<Child> _Children;
public ObservableCollection<Child> Children
{
get { return _Children; }
set
{
_Children = value;
OnPropertyChanged("Children");
_Children.CollectionChanged +=handler;
}
}
private void handler(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//What to do ?
}
public ChildViewModel()
{
Children = new ObservableCollection<Child>(ChildService.GetAllChild());
}`
Edit #lezhkin11
Yes I think that is the problem. But I have no idea how to fix it
Code behind MainView
void MainView_Loaded(object sender, RoutedEventArgs e)
{
mainViewModel = new MainViewModel();
DataContext = mainViewModel;
}
Code behind ChildView
void ChildView_Loaded(object sender, RoutedEventArgs e)
{
childViewModel = new ChildViewModel();
DataContext = childViewModel;
}
A button allow to do a new search:
private void BtnRefresf_Click(object sender, RoutedEventArgs e) // Search
{
childViewModel.Search();
}
Then a method in the childViewModel will use the service to give a new value to the Observable collection (when I debug, I can reach the OnpropertyChanged of the collection)
public void Search()
{
ObservableCollection<Child> collec = new ObservableCollection<Child>(ChildService.GetAllChild());
Children = collec;
}
Your DataGrid is bound to ChildViewModel inside MainViewModel (instance 1). While ChildView has it's own ChildViewModel (instance 2).
You have to make sure that both controls reference to the same instance.
1) Remove completely public ChildViewModel childViewModel { get; set; } from MainViewModel
2) Give a name to the ChildView.
<vi:ChildView x:Name="MyChild" />
3) Bind DataGrid's properties to correct view-model
<DataGrid ItemsSource="{Binding DataContext.Children, ElementName=MyChild, Mode=TwoWay}" />
Also, I would recommend initializing DataContext directly in constructor.
Loaded event is too late - as result you always have lot's of XAML binding exceptions that affect performance (even can lead to unexpected behavior)

Listbox scroll viewer - default scroll to bottom

I have a ListBox which has to be scrolled to bottom automatically. In my application, I have to detect if some items were already visibled to user and to do some business logic if so. I am using virtualization here which calls item (vm) properties only if it is visible.
For auto scroll, i am using listbox.ScrollIntoView(listbox.SelectedItem); which works perfect, the problem is that the ScrollIntoView will run only after the ListBox is already loaded and rendered which means that it first displays some items from its beginning and after that it will scroll to bottom... it is undesirable for me. I just want to scroll immediately to bottom (before the ListBox is rendered).
Here is my behavior for auto scroll to bottom:
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
void AssociatedObject_SelectionChanged(object sender, EventArgs e)
{
if (sender is ListBox)
{
ListBox listbox = (sender as ListBox);
if (listbox.SelectedItem != null)
{
listbox.Dispatcher.BeginInvoke((Action)(() =>
{
listbox.UpdateLayout();
if (listbox.SelectedItem != null)
{
listbox.ScrollIntoView(listbox.SelectedItem);
}
}));
}
}
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.SelectionChanged -= AssociatedObject_SelectionChanged;
}
My ListBox is setted to IsSynchronizedWithCurrentItem="True" and its ItemsSource is binded to ICollectionView where i am using MoveCurrentToLast.
So the question is: is there any way how to scroll to bottom without rendering its top first?
I've reproduced your attached command as a
public class MyBehavior : Behavior<ListBox>
{
to a XAML
<ListBox SelectedItem="SelCust" Name="MyListBox" Loaded="MyListBox_Loaded" IsSynchronizedWithCurrentItem="True" DisplayMemberPath="Name" ItemsSource="{Binding Customers}">
<i:Interaction.Behaviors>
<local:MyBehavior/>
</i:Interaction.Behaviors>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding Path=LoadCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
where I've also added a binding for the Load event to the ViewModel
public CustomerViewModel()
{
IList<Customer> customers = Customer.GetCustomers().ToList();
_customerView = CollectionViewSource.GetDefaultView(customers);
_customerView.MoveCurrentToLast();
_customerView.CurrentChanged += CustomerSelectionChanged;
}
private void CustomerSelectionChanged(object sender, EventArgs e)
{
// React to the changed selection
Debug.WriteLine("Here");
var sel = (sender as CollectionView).CurrentItem;
if ( sel!= null)
{
//Do Something
}
}
private DelegateCommand loadCommand;
public ICommand LoadCommand
{
get
{
if (loadCommand == null)
{
loadCommand = new DelegateCommand(VMLoad);
}
return (ICommand)loadCommand;
}
}
bool IsLoaded = false;
private void VMLoad(object obj)
{
IsLoaded = true;
}
and in the code-behind
public MainWindow()
{
InitializeComponent();
DataContext = new CustomerViewModel();
}
private void MyListBox_Loaded(object sender, RoutedEventArgs e)
{
MyListBox.ScrollIntoView(MyListBox.Items[MyListBox.Items.Count - 1]);
}
When I debug it, I see that the this is the sequence of events fired:
CurrentChanged wiht the last item of the collection
Loaded handler in the code-behind
LoadCommand in the ViewModel and only after that
ScrollIntoView from the AssociatedObject_SelectionChanged
So basically I'm suggesting a couple of things:
add (another) ScrollIntoView (for the last item of the collection) from the Loaded handler in the
code-behind
for whatever action you need to perform when you have to detect if some items are already visible to user, first check IsLoaded to exclude any transient effect
Why not simply scroll to the last value in your collection after you set the DataContext or ItemSource? No data will render until you set your data context, and until you exit the constructor. To my understanding if you do the following to steps in sequence in the constructor, it should work as expected.
listBox.DataContext = _items;
listBox.ScrollIntoView(_items.Last());

WPF MVVM firing code based on Tab SelectedValue, rather than SelectedIndex

In WPF with MVVM it's easy to fire some code when the user changes the tab.
<TabControl Margin="0 5 5 5" Background="#66F9F9F9" SelectedIndex="{Binding TabIndex}">
And then in the ViewModel:
private int _tabIndex;
public int TabIndex
{
get { return _tabIndex; }
set
{
if(_tabIndex != value)
{
_tabIndex = value;
OnPropertyChanged("TabIndex");
if(value == 1)
{
//do something
}
}
}
}
But I'm vaguely uncomfortable with this. What if another developer happens along later and adds another tab in the "1" position. If this is application-critical code (which it is), things will break spectacularly.
Danger can be minimized with unit tests, of course. But it made me wonder: is this seen as bad practice? And is there a way of doing this that allows you to refer to the Tab with a string, rather than an int? I tried noodling with binding to the SelectedValue property, but nothing seemed to happen when the tabs were changed.
You could make a behavior for TabItem, listening for changes to the IsSelected dependency property, and raises a Command when the tab is selected. This can be extended to any number of tabs, each which invokes different commands in the viewmodel. You could also supply a command parameter for any optional context:
class TabSelectedBehavior : Behavior<TabItem>
{
public static readonly DependencyProperty SelectedCommandProperty = DependencyProperty.Register("SelectedCommand", typeof(ICommand), typeof(TabSelectedBehavior));
public ICommand SelectedCommand
{
get { return (ICommand)GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
private EventHandler _selectedHandler;
protected override void OnAttached()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null)
{
_selectedHandler = new EventHandler(AssociatedObject_SelectedChanged);
dpd.AddValueChanged(AssociatedObject, _selectedHandler);
}
base.OnAttached();
}
protected override void OnDetaching()
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(TabItem.IsSelectedProperty, typeof(TabItem));
if (dpd != null && _selectedHandler != null)
{
dpd.RemoveValueChanged(AssociatedObject, _selectedHandler);
}
base.OnDetaching();
}
void AssociatedObject_SelectedChanged(object sender, EventArgs e)
{
if (AssociatedObject.IsSelected)
{
if (SelectedCommand != null)
{
SelectedCommand.Execute(null);
}
}
}
}
XAML
<TabControl>
<TabItem Header="TabItem1">
<i:Interaction.Behaviors>
<local:TabSelectedBehavior SelectedCommand="{Binding TabSelectedCommand}"/>
</i:Interaction.Behaviors>
</TabItem>
<TabItem Header="TabItem2">
</TabItem>
</TabControl>
In a similar fashion you could also make a behavior for the TabControl, which turns the SelectionChanged event into a command, and pass the Tag object of the selected TabItem as command parameter.
As with all collection controls, the best way to maintain the selected item is to use the SelectedItem property. If you data bind a property of the relevant data type to the TabControl.SelectedItem property, then you'll still be able to tell which tab is selected and select a different one from the view model.
The only problem with this method is that you'll also need to use the TabControl.ItemsSource property to set up the TabItems:
<TabControl ItemsSource="{Binding YourDataItems}" SelectedItem="{Binding YourItem}" />
If you want to try this, then you should know that defining the TabItems can be a little bit confusing. Please refer to the answer from the How to bind items of a TabControl to an observable collection in wpf? question for help with that.

WPF UserControl DependencyProperty and INotifyPropertyChanged on an ObservableCollection

In an attempted MVVM implimentation, I have a UserControl implementing INotifyPropertyChanged. OnFilePathsChanged seems to be called after FilePaths has the new value, so it just copies it over again. If I don't implement INotifyPropertyChanged OnPropertyChanged the VIEWMODEL never gets updated. I have read that using INotifyPropertyChanged together with a DependencyProperty is redundant but I can't figure out the best way to do this. What am I missing. Is there a cleaner way to implement a UserControl with a bound ObservableCollection?
UserControl code
private void LoadFileMenuItem_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
FilePaths.Add(dialog.FileName);
OnPropertyChanged("FilePaths");
}
}
private void InitFilePathsProperty()
{
this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(FilePaths_PropertyChanged);
}
void FilePaths_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.SetValue(FilePathsProperty, ImageFilePaths);
}
public static readonly DependencyProperty FilePathsProperty = DependencyProperty.Register("FilePaths", typeof(ObservableCollection<string>), typeof(FileDisplayControl),
new PropertyMetadata(OnImageFilePathsChanged));
private static void OnFilePathsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue != null)
{
var myFileDisplayControl = obj as FileDisplayControl;
if (myFileDisplayControl != null &&
myFileDisplayControl.FilePaths != null)
{
FilePaths = this.GetValue(args.Property) as ObservableCollection<string>;
}
}
}
public ObservableCollection<string> FilePaths
{
get { return this.GetValue(FilePathsProperty) as ObservableCollection<string>; }
set { this.SetValue(FilePathsProperty, value); }
}
In the VIEWMODEL
public ObservableCollection<string> ViewModelFilePaths
{
get { return _viewModelFilePaths; }
set { _viewModelFilePaths = value; }
}
It's a bit unclear as to what you're actually asking, but I think that this will help you whatever you're asking. If you have a view model that is data bound to a UserControl.DataContext, you can still data bind to any DependencyPropertys defined in that control using a RelativeSource Binding. Take this example from inside the UserControl:
Binding to the UserControl DependencyProperty:
<ListBox ItemsSource="{Binding FilePaths, RelativeSource={RelativeSource
AncestorType={x:Type YourPrefix:YourUserControl}}}" ... />
Binding to the data bound view model property:
<ListBox ItemsSource="{Binding ViewModelFilePaths}" ... />
You should implement the INotifyPropertyChanged interface in the view model, not the UserControl... at least, it is more customary to declare DependencyPropertys in a UserControl. However, if you have declared normal CLR properties that you want to data bind to in the UserControl, then you should implement the INotifyPropertyChanged interface there too.

WPF. ListBox. How to limit maximum of selected items to 2?

I need to limit the number of selected items in a ListBox to 2. How to do this? Is it possible to avoid the usage of events and do this in XAML?
There's no built-in way to do this as far as I'm aware, so you're going to have to write some code.
You could set your ListBox as multi-select by setting SelectionMode to Multiple or Extended (See here for the difference) and attach onto the SelectionChanged event. When the event is raised, modify the selection how you see fit (how you do it would depend on how you wanted it to work... if they select a third one, does it get removed right away? or does the first one get removed - effectively FIFO or LIFO removal).
Edit:
My bad, I had linked to the wrong article.
I made a behavior of this where I can bind the number of elements I want to have selected to a dependencyproperty.
It's used like this, attached to a ListView
<i:Interaction.Behaviors>
<behaviors:LimitSelectionBehavior Limit="2" />
</i:Interaction.Behaviors>
And here is the Behavior class. The Remove could be modified to unselect down to the Limit, now it just unselects all newly added.
public class LimitSelectionBehavior : Behavior<ListView>
{
public static readonly DependencyProperty LimitProperty;
static LimitSelectionBehavior()
{
LimitProperty = DependencyProperty.Register("Limit", typeof(int), typeof(LimitSelectionBehavior), new PropertyMetadata(default(int)));
}
public int Limit
{
get { return (int) GetValue(LimitProperty); }
set { SetValue(LimitProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += OnSelectionChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (AssociatedObject.SelectedItems.Count <= Limit)
return;
foreach (var added in e.AddedItems)
{
AssociatedObject.SelectedItems.Remove(added);
}
}
}
Following is an example of how to restrict selection to two items only
For the ListView defined in the following XAML
<ListView x:Name="MyListView" ItemsSource="{Binding Path=ParentSection.MyListItems}" BorderThickness="0"
SelectionMode="Multiple" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
mvvm:FrameworkElementBehaviors.IgnoreMouseWheel="True"
SelectionChanged="MyListView_SelectionChanged">
<ListView.View>
<!--Your list view content here -->
</ListView.View>
</ListView>
The event would be something like following
public void MyListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.MyListView.SelectedItems.Count > 2)
{
this.MyListView.SelectedItems.RemoveAt(0);
}
}

Categories