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());
Related
Question:
How do we access the bound command assigned to the MenuItem inside our custom behavior? The ContextMenu isn't a part of the visual tree and doesn't bind until a click event which never occurs due to being suppressed in the custom behavior.
Purpose
I have a custom behavior using Microsoft.Xaml.Behaviors that is intended to only display the context menu if the user clicks on an object in the ListView. I want to modify the command parameter using the custom behavior while accessing the reference ICommand in the view's markup.
Code:
Custom Behavior (Microsoft.Xaml.Behaviors):
public class RightClickContextMenuBehavior : Behavior<ListView>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.ContextMenuOpening += AssociatedObject_ContextMenuOpening;
AssociatedObject.PreviewMouseRightButtonDown += AssociatedObject_PreviewMouseRightButtonDown;
AssociatedObject.PreviewMouseRightButtonUp += AssociatedObject_PreviewMouseRightButtonUp;
}
protected override void OnDetached()
{
base.OnDetached();
AssociatedObject.ContextMenuOpening -= AssociatedObject_ContextMenuOpening;
AssociatedObject.PreviewMouseRightButtonDown -=
AssociatedObject_PreviewMouseRightButtonDown;
AssociatedObject.PreviewMouseRightButtonUp -= AssociatedObject_PreviewMouseRightButtonUp;
}
private void AssociatedObject_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
//Store the click locations so that we can determine drag on mouse move.
originPoint = e.GetPosition(null);
//Grab the item from the ListView
var listViewItem = TryFindFromPoint<ListViewItem>((UIElement)sender,
e.GetPosition(AssociatedObject));
if (listViewItem == null)
return;
itemReference = listViewItem;
}
private void AssociatedObject_PreviewMouseRightButtonUp(object sender,
MouseButtonEventArgs e)
{
if (itemReference != null)
{
// Only display the context menu if the user clicked on the object
// inside the listview, not the listview itself.
var targetPosition = e.GetPosition((UIElement)itemReference);
HitTestResult hitResult = VisualTreeHelper.HitTest((UIElement)itemReference,
targetPosition);
if (hitResult != null && sender is ListView)
{
var listView = sender as ListView;
//Set the context menu's visibility flag
listView.ContextMenu.IsOpen = true;
foreach (var item in listView.ContextMenu.Items)
{
if (item is MenuItem)
{
var customMemuItem = item as MenuItem;
//Apply the item the was in our hitbox as context.
customMemuItem.CommandParameter = itemReference.DataContext;
/*
ISSUE:
Bindings here are always null, how do I force the bound
ICommand in the XAML markup to be available here. I'd like
to use the Command binding from the markup, and functionally
apply my DataContext customMemuItem.Command is null
*/
}
}
e.Handled = true; //Handle the bubble
}
}
else
{
if (sender is ListView)
{
//Hide the context menu.
var listView = sender as ListView;
listView.ContextMenu.IsOpen = false;
e.Handled = true;
}
}
}
}
View:
<ListView Grid.Row="1"
ItemsSource="{Binding Path= ItemQueue, IsAsync=True}"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<behaviors:Interaction.Behaviors>
<commandbehaviors:RightClickContextMenuBehavior />
</behaviors:Interaction.Behaviors>
<ListView.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext,
RelativeSource={RelativeSource Self}}">
<MenuItem Header="Context Menu Command"
Command="{Binding Source={x:Type models:MyViewModel},
Path=BindingContext.MenuTestCommand}"
CommandParameter="{Binding}"/>
</ContextMenu>
</ListView.ContextMenu>
[...]
</ListView>
Edit #1:
We're using MVVM where ListView is in:
public partial class MyViewModelView : UserControl
{
public DocumentTileManagementView()
{
InitializeComponent();
}
}
public class MyViewModel : ViewModelBase
{
public MyViewModel()
{
MenuTestCommand = new ApplicationRelayCommand(MenuTestCommandBehavior);
}
public ObservableCollection<ObjectViewModel> ItemQueue
{
get
{
return _ItemQueue;
}
set
{
this.MutateVerbose(ref _ItemQueue, value, this.RaisePropertyChanged());
}
}
private ObservableCollection<MyDataObject> _ItemQueue= new ObservableCollection<MyDataObject>();
public ICommand MenuTestCommand { get; }
private async void MenuTestCommandBehavior(object obj)
{
}
}
I haven't found a way to invoke the binding without allowing the inital right-click event chain on the ListView.
I have a working solution where I allow the first right click event and override the behavior in each subsequent click. I'd like to invoke whatever is occurring during that first right click in the custom (Microsoft.Xaml.Behaviors) behavior.
Instead of attempting to override the entire event chain I've attached to the ContextMenuOpening event. On the first PreviewMouseRightButtonUp we allow the event chain to complete binding all parameters and commands from mark up. The ContextMenuOpening event allows for initial hitbox configuration and CommandParameter assignment.
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.ContextMenuOpening += AssociatedObject_ContextMenuOpening;
[...]
}
private void AssociatedObject_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
if (!isFirstClick)
{
isFirstClick = true;
e.Handled = false;
AssociatedObject.ContextMenu.Visibility = Visibility.Collapsed;
return;
}
else
{
[...]
}
}
private void AssociatedObject_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (AssociatedObject.Items.Count > 0)
{
var listViewItem = TryFindFromPoint<ListViewItem>((UIElement)e.OriginalSource, new Point(e.CursorLeft, e.CursorTop));
foreach (var item in AssociatedObject.ContextMenu.Items)
{
if (item is MenuItem && listViewItem != null)
{
var customMemuItem = item as MenuItem;
customMemuItem.CommandParameter = listViewItem.DataContext;
}
}
AssociatedObject.ContextMenu.Visibility = Visibility.Visible;
AssociatedObject.ContextMenu.IsOpen = true;
}
else
{
AssociatedObject.ContextMenu.Visibility = Visibility.Collapsed;
AssociatedObject.ContextMenu.IsOpen = false;
}
}
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;
}
}
}
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.
I use the ComboBox for binding to string property of view model. I choose the ComboBox instead of TextBox, because i want to have an option to choose from the list (as a suggestion), but I don't want to change the selected text if the ItemsSource changes.
I tried to set the IsSynchronizedWithCurrentItem property to false, but when the list of suggestions change (at the position of the selected text), the Text changes to empty.
It seems that the ComboBox has remembered that the entered text was also in the list and when this item disappears, the Text property is also cleared.
So my question is: Is that a bug, or am I doing something wrong?
If it is a bug, could you suggest some work around?
I created a sample project which preproduces this:
in XAML:
<Window x:Class="TestProject1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ComboBox IsSynchronizedWithCurrentItem="False" ItemsSource="{Binding Items}"
IsEditable="True" Text="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" Margin="10,39,0,0" VerticalAlignment="Top" Width="120"/>
<Button Click="Button_Click" Content="Update list"
HorizontalAlignment="Left" Margin="10,82,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
In code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow() {
InitializeComponent();
this.DataContext = this;
Items = new List<string>() { "0", "1", "2" };
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private List<string> _items;
public List<string> Items {// I use IEnumerable<string> with LINQ, but the effect is the same
get { return _items; }
set {
if (_items != value) {
_items = value;
RaisePropertyChanged("Items");
}
}
}
private string _selectedText;
public string SelectedText {
get { return _selectedText; }
set {
if (_selectedText != value) {
_selectedText = value;
RaisePropertyChanged("SelectedText");
}
}
}
private void Button_Click(object sender, RoutedEventArgs e) {
var changed = Items.ToList();//clone
int index = changed.IndexOf(SelectedText);
if (index >= 0) {
changed[index] += "a";//just change the currently selected value
}
Items = changed;//update with new list
}
}
This is my fix for that issue:
public class ComboBox : System.Windows.Controls.ComboBox
{
private bool ignore = false;
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (!ignore)
{
base.OnSelectionChanged(e);
}
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
ignore = true;
try
{
base.OnItemsChanged(e);
}
finally
{
ignore = false;
}
}
}
After your ItemsSource has changed, raise a property changed on your selected text to refresh the UI.
So in your Items collection setter, make the change:
RaisePropertyChanged("Items");
RaisePropertyChanged("SelectedText");
EDIT: in your example you aren't just changing the ItemSource, you are changing the text of the item that is the currently selected one but having a text binding on the old text. What are you expecting to see/happen? Are you wanting the selected item to stay the same, even if its text changes?
Modify Button_Click like this (commented lines are new):
private void Button_Click(object sender, RoutedEventArgs e)
{
string tempCopy = SelectedText; // Create a copy of the current value
var changed = Items.ToList();
int index = changed.IndexOf(SelectedText);
if (index >= 0)
{
changed[index] += "a";
}
Items = changed;
SelectedText = tempCopy; // Replace the selected text with the copy we made
}
All this does is makes a copy of SelectedText before Items changes, and then replaces it once the change has been made.
Copy SelectedText
Modify items source
Replace SelectedText with the copy
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);
}
}