How do I delete a selected ListViewItem from a WPF ListView when the ItemsSource is set to a DataView? I can get the ListViewItem that was selected and then how do remove the actual row in the DataView?
DataView dv = (DataView)myListView.ItemsSource;
ListViewItem lvi = (ListViewItem)myListView.ItemContainerGenerator.ContainerFromItem(myListView.SelectedItem);
<Delete ListViewItem here>
When you bind your collection to the listview, use ListCollectionView instead of DataView. Can be easily done like this (where dataView is of type DataView):
ListCollectionView lcv = new ListCollectionView(dataView);
myListView.ItemsSource = lcv;
Now when you need to delete any object, just do this:
ListCollectionView lcv = (ListCollectionView) myListView.ItemsSource;
lcv.Remove(myListView.SelectedItem);
And after deleting, just refresh the view:
lcv.Refresh();
or
((ListCollectionView)myListView.ItemsSource).Refresh();
Consider using the M-V-VM pattern to separate the notion of removing an item from your list of data objects and DIRECTLY removing them from your current UI implementation. The two do not need to know about each other, aside from Bindings.
When you use the MVVM pattern, expose a boolean "IsSelected" property in your ViewModel.
public class SimpleViewModel : BaseViewModel //For INotifyPropertyChanged, etc
{
public IList<SimpleBusinessObject> ViewModelItems;
public SimpleViewModel()
{
ViewModelItems = new ObservableList<SimpleBusinessObjectViewModel>();
}
}
public class SimpleBusinessObjectViewModel
{
public bool ViewModelIsSelected { get; set; }
public SimpleBusinessObjectViewModel()
{
ViewModelIsSelected = false;
}
}
Next, in your View try something like this:
<Style TargetType="{x:Type ListViewItem}">
<Style.Triggers>
<Setter Property="IsSelected" Value="{Binding ViewModelIsSelected}"
</Style.Triggers>
</Style>
<ListView ItemsSource={Binding ViewModelItems}>
//here you can insert how you want to display a ListViewItem
</ListView>
This will let you add, edit, and remove items in your ViewModel's List -- just like if it were the actual ListView. From here, you can also check each item's IsSelected (that responds to mouse interactions with the ListView) without actually checking the ListViewItem. This will be a much cleaner, maintainable solution.
Related
How do I prevent a ListViewItem from being selected when I click on an item and press (Down/Up) Arrow? I don't want to disable it, I just want it to not be selectable on Shift+Arrow. Some of my listviewitems need to be selectable and some others not.
Sample:
[ListViewItem 1]
[ListViewItem 2]
[ListViewItem 3]
User clicks on ListviewItem1, it gets selected.
User then presses SHIFT+DOWN ARROW,=> ListViewItem 2 is not selected.
At this point either ListViewItem3 is selected or it might take another SHIFT+DOWN ARROW to select ListViewItem3 (it doesn't matter as long as it gets selected sometime).
(In my project the actual ListViewItem 2 is actually NOT a regular item on 1 row, it is a vertical item that spans several rows in just one column. My ListView source has a compositecollection of very different items; I am trying to select what appears to be 'regular row items' only)
You can accomplish this by using a Property of your class such as IsSelected. Here is some sample code for what that Property would look like.
public bool IsSelected
{
get
{
return isSelected; //return the value of isSelected
}
set
{
if (isSelectable) //if the item is allowed to be selected
{ //then update the value of isSelected
isSelected = value;
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
In this example it is assumed that "isSelectable" is a value that is set in the constructor when your class is being initialized. If you don't already have INotifyPropertyChanged implemented, it needs to be... and the PropertyChanged event needs to be declared.
In addition, you will also want a Style that binds the selection of ListView items to this property. Here is an example of a Style that could accomplish this.
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListView.Resources>
You can take this one step further, and also bind to the Focusable Property. This will make it so that when you press SHIFT+DOWN it will skip any items that you cannot select and select the next item that is selectable. I would accomplish this in a manner similar to the following.
public bool Focusable
{
get
{
return isSelectable; //if the item is selectable, then it is focusable
}
}
You would also need to create a binding for this in your Style by creating an additional Setter. This could be done the following way, inside the same Style as before.
<Setter Property="Focusable" Value="{Binding Focusable}"/>
Try it both with and without that last Setter and see which implementation suits your needs best.
--------- UPDATE ---------
If you would like to only select items on mouse click, you could do so by subscribing to the ListView PreviewMouseLeftButtonDown event. Inside of the event handler, you would need to first get the clicked item, and call a function on your class which will override the selection. Here is an example of how that is done.
This function would exist in your UI code-behind and must be subscribed to by your ListView:
private void myListView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DependencyObject dep = (DependencyObject)e.OriginalSource;
while ((dep != null) && !(dep is ListViewItem))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
return;
object obj = myListView.ItemContainerGenerator.ItemFromContainer(dep);
if (obj is MyClass)
{
foreach (MyClass i in myListView.Items)
i.OverrideSelect(false); //unselect all items
MyClass item = (MyClass)obj;
item.OverrideSelect(true); //select the item clicked
}
}
(Note: replace "MyClass" with your class type).
This function would exist in your class file:
public void OverrideSelect(bool selected)
{
isSelected = selected;
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
I've a datagrid with an ItemsSource of ObservableCollection (OC) of objects. When an item's property changes, I want to work on the OC itself .
E.g. I've an item which is approved for uploading to our database. However, I need to loop through the OC to check if other items exist in the collection which already fit the set criteria, so that I may actually not have to upload the selected item.
On the datagrid, when I tick the checkbox of an item, it will change the boolean value (e.g. "IsToUpload") of the item, and an event should trigger on the property change.
I'm assuming I will then need to 'bubble up' my event notifications to the datagrid/mainwindow class, where I can then work on the OC. How may I do this, and if this is not the correct way, what should I be doing?
I've followed Aran Mulholland's class structure to colour my rows dynamically: Coloring WPF DataGridRows one by one
So my class structure is roughly as follows:
MainWindow -> DataGrid
-> ObservableCollection<ItemObjectViewModel:NotificationObject>
ItemObject : INotifyPropertyChanged //this class is where I
//store my item variables. It is referenced through properties
//in the ItemObjectViewModel.
Event bubling \ routing etc works for dependency objects in a visual \ logical tree. Your NotificationObject is not a dependency object and neither is it hosted in the visual tree.... What we have in visual tree are the checkboxes (that are bound to your NotificationObject).
Non MVVM
In you DataGrid you would have to Tag your Checkboxes with some identification and then use ButtonBase.Click="" event at datagrid level which will be handled for any click event bubbled for any button based eleemnt (such as buttons, menuitems, togglebuttons, checkboxes, radioboxes, comboboxes) that gets clicked in the entire visual tree of the datagrid.
In the handler verify if the e.OriginalSource is a checkbox and that its Tag is same as the identification value we have set in the XAML of the datagrid. That way we know that the CheckBox is clicked.
E.g.
<DataGrid AutogenerateColumns="False"
ItemsSource="{Binding NotificationObjectCollection}"
ButtonBase.Clicked="OnNotificationCheckBoxClicked">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsClicked}"
Header="Click?">
<DataGridCheckBoxColumn.ElementStyle>
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Tag" Value="IsClickCheckBox" />
</Style>
</DataGridCheckBoxColumn.ElementStyle>
</DataGridCheckBoxColumn>
</DataGrid.Columns>
</DataGrid>
private void OnNotificationCheckBoxClicked
(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is CheckBox)
{
if (((CheckBox)e.OriginalSource).Tag == "IsClickCheckBox")
{
var notificationObject
= ((CheckBox)e.OriginalSource).DataContext
as NotificationObject;
if (notificationObject.IsClicked) { }
else { }
}
}
}
MVVM
The only way MVVM can notify the ancestor object in the visual is by using Command execution as the underlying NotificationObject gets checked (setter is called) we execute the command supplied to the NotificationObject.
Use the weak reference based RelayCommand or DelegateCommand (as available on the internet) for this purpose.
Add a new NotificationObject constructor
private ICommand _isClickedCommand;
public NotificationObject(ICommand isClickedCommand)
{
_isClickedCommand = isClickedCommand;
}
private bool _isClicked;
public bool IsClicked
{
get
{
return _isClicked;
}
set
{
if (_isClicked != value)
{
_isClicked = value;
OnPropertyChanged("IsClicked");
isClickedCommand.Execute(this);
}
}
}
Using the notification object
public class ItemObjectViewModel
{
private DelegateCommand<NotificationObject>
_notificationObjectClickedCommand
= new DelegateCommand<NotificationObject>(
OnNotificationObjectCommandExecute);
....
private void PopulateCollection()
{
NotificationObjectCollection
= new ObservableCollection<NotificationObject>();
NotificationObjectCollection.Add(
new NotificationObject(_notificationObjectClickedCommand));
}
private void OnNotificationObjectCommandExecute(
NotificationObject notificationObject)
{
if (notificationObject.IsClicked) { }
else { }
}
}
You can also achieve the ICommand based behavior in non MVVM scenario by using 'RoutedCommand'
Let me know if this helps...
I have a datagrid that is multi-select enabled. I need to change the selection in the viewmodel. However, the SelectedItems property is read only and can't be directly bound to a property in the viewmodel. So how do I signal to the view that the selection has changed?
Andy is correct. DataGridRow.IsSelected is a Dependency Property that can be databound to control selection from the ViewModel. The following sample code demonstrates this:
<Window x:Class="DataGridMultiSelectSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<StackPanel>
<tk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" EnableRowVirtualization="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</tk:DataGrid.Columns>
<tk:DataGrid.RowStyle>
<Style TargetType="tk:DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</tk:DataGrid.RowStyle>
</tk:DataGrid>
<Button Content="Select Even" Click="Even_Click" />
<Button Content="Select Odd" Click="Odd_Click" />
</StackPanel>
</Window>
using System.ComponentModel;
using System.Windows;
namespace DataGridMultiSelectSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new[]
{
new MyViewModel {Value = "Able"},
new MyViewModel {Value = "Baker"},
new MyViewModel {Value = "Charlie"},
new MyViewModel {Value = "Dog"},
new MyViewModel {Value = "Fox"},
};
}
private void Even_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[]) DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i%2 == 0;
}
private void Odd_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[])DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i % 2 == 1;
}
}
public class MyViewModel : INotifyPropertyChanged
{
public string Value { get; set; }
private bool mIsSelected;
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (mIsSelected == value) return;
mIsSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Be sure to set EnableRowVirtualisation="False" on the DataGrid element, else there's a risk that the IsSelected bindings fall out of kilter.
I haven't worked with the DataGrid much, but one technique that works for the ListView is to bind to the IsSelected property of the individual ListViewItem. Just set this to true for each object in your list, and then it will get selected.
Maybe the object that represents a row in the DataGrid also has an IsSelected property, and can be used in this way as well?
Guys, thanks for the help. My problem was solved. I think the problem is pretty common for new WPF developers, so I will restate my problem and as well as the solution in more details here just in case someone else runs into the same kind of problems.
The problem: I have a multi-select enabled datagrid of audio files. The grid has multiple column headers. The user can multi-select several row. When he clicks the Play button, the audio files will be played in the order of one the columns headers (say column A). When playback starts, the multi-select is cleared and only the currently playing file is highlighted. When playback is finished for all files, the multi-selection will be re-displayed. The playback is done in the viewmodel. As you can see, there are two problems here: 1) how to select the currently playing file from the viewmodel, and 2) how to signal to the view from the viewmodel that playback is finished and re-display the multi-selection.
The solution: To solve the first problem, I created a property in the viewmodel that is bound to the view's SelectedIndex property to select the currently playing file. To solve the second problem, I created a boolean property in the view model to indicate playback is finished. In the view's code behind, I subscribed the the boolean property's PropertyChanged event. In the event handler, the view's SelectedItems property is re-created from the saved multi-selection (the contents of SelectedItems was saved into a list and SelectedItems was cleared when playback started). At first, I had trouble re-creating SelectedItems. It turned out the problem was due to the fact that re-creation was initiated through a second thread. WPF does not allow that. The solution to this is to use the Dispatcher.Invoke() to let the main thread do the work. This may be a very simple problem for experienced developers, but for newbies, it's a small challenge. Anyway, a lot of help from different people.
Just use SelectedItems on any MultiSelector derived class , and use methods Add, Remove, Clear on IList it returns .
I'm horrible at this WPF thing, so bear with me.
I'm using the Xceed DataGrid for WPF, and I need to know when someone selects a row, but I can't figure out how to do it. I'm sure I need to add some XAML to enable this, but I can't figure out what I should do.
I use a MVVM approach and therefor favor data binding. I will bind the SelectedItem property to a SelectedItem property on my ViewModel object for the grid.
<xcdg:DataGridControl x:Name="grid" SelectedItem="{Binding SelectedItem}">
</xcdg:DataGridControl>
Then on your property setter can do what ever is necessary upon change in the SelectedItemChanged() method.
private IMyItem _selectedItem;
public IMyItem SelectedItem
{
get { return _selectedItem; }
set {
_selectedItem = value;
OnPropertyChanged("SelectedItem");
SelectedItemChanged();
}
}
I'm actually struggling a bit with the same thing myself, except I have a prerequisite that the selection notification be done via an ICommand; however, if you do not have this need, you can wire up the SelectionChanged event handler. It's pretty elementary stuff, but I'll include the code just in case:
XAML:
<Grid>
<DataGrid:DataGridControl x:Name="gridControl" SelectionChanged="gridControl_SelectionChanged">
<!-- Content -->
</DataGrid:DataGridControl>
</Grid>
Code-behind:
private void gridControl_SelectionChanged(object sender, Xceed.Wpf.DataGrid.DataGridSelectionChangedEventArgs e)
{
var selectedIndex = gridControl.SelectedIndex; // int index
var selectedItem = gridControl.SelectedItem; // instance of bound object
var selectedItems = gridControl.SelectedItems; // IList of bound objects
}
All that said, I'm very interested to hear if there are any elegant solutions for getting the selected row from an Xceed DataGrid with an ICommand (in my case, I'm using anonymous types, which can make a difference)...
You don't have to write complicated code for something simple... although it can become tedious, here is some code for you. I hope this helps:
<Style TargetType="xcdg:DataRow">
<EventSetter Handler="dr_PreviewMouseDown" Event="PreviewMouseDown" />
</Style>
void dr_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
DataRow dr = sender as DataRow;
Debug.WriteLine(sender);
}
So here's what I came up with
System.ComponentModel.DependencyPropertyDescriptor gridItemsSourceDescriptor = System.ComponentModel.DependencyPropertyDescriptor.FromProperty(DataGridControl.SelectedItemProperty, typeof(DataGridControl));
gridItemsSourceDescriptor.AddValueChanged(dgBaxRuns, HandleSelectionChanged);
I made for me a easiest way.
<xctk:MaterialButton Margin="5,0,5,0" Grid.Column="3" Content="Szűrt sorok kijelölése" Command="{Binding SelectFilteredRowsCommand}" CommandParameter="{Binding ElementName=MyDataGrid}" />
So, i send my datagrid with my commandparameter to the viewmodel.
public RelayCommand<object> SelectFilteredRowsCommand { get; set; }
SelectFilteredRowsCommand = new RelayCommand<object>((o) =>
{
var datagrid = o as DataGridControl;
if (datagrid != null)
{
var datagriditems = datagrid.Items.Cast<SelectableProduct>();
foreach (SelectableProduct selectableProduct in datagriditems)
{
selectableProduct.IsSelect = true;
}
}
});
And convert back to datagrid itemsoruce type.
i am trying to make an item template where some of the field in my stack panel can be empty. When it's empty, I would like to set the visiblility to collapsed. I tried putting triggers but it doesn't seem to work and I am not very familiar with this part of WPF
Also, I would like to change the color of the background of this item when a specific value in my binding is true. Is it the same thing?
Thanks.
Using a ViewModel is one approach to solving this kind of problem.
The if your data was stored in an Item class you would make an ItemViewModel to wrap the Item for display in your items control. The ViewModel class would implement INotifyProperty changed in order to update the display and the setters would raise the PropertyChanged event passing the appropriate property name. You can also raise property changed events for as many interrelated changed fields as necessary.
Suppose you wanted Item.Description to display in a collapsed field when Description is empty. Your ViewModel properties could look like this
public string Description
{
get { return mItem.Description; }
set { mItem.Description = value; Notify("Description"); Notify("DescriptionVisibility"); }
}
public Visibility DescriptionVisibility
{
get { return string.IsNullOrEmpty(mItem.Description) ? Visibility.Visible : Visibility.Collapsed; }
}
In the XAML bind the text property to Description and the Visibility property to DescriptionVisibility.
If you want to hide an item if it's content is null, you have to redefine the ControlTemplate of its ListBoxItem (or ListViewItem or something else depending on which item container you're using) and use triggers that target the DataContext, like:
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
However, I'd suggest that you use the Filter delegate on your CollectionView to exclude your empty items from your view directly, to avoid collapsing unused items.
For example to exclude null objects, in your code behind, use:
CollectionViewSource.GetDefaultView(yourCollection).Filter = o => o != null;