listview not refreshed when using databinding - c#

I have a model class, Book, which contains a Keywords property:
public class Book : INotifyPropertyChanged
{
private ObservableCollection<string> _keywords;
...
public ObservableCollection<string> Keywords
{
get => _keywords;
set
{
_keywords = value;
OnPropertyChanged("Keywords");
}
}
}
and in my MainPage I have 2 components : a list View and a combobox whose each entry is a checkBox:
<ComboBox
x:Name="cbb_Keywords"
Grid.Column="2"
Width="300"
Margin="5,0,0,0"
HorizontalAlignment="Left"
ItemsSource="{Binding Source={StaticResource AllBooks}}"
DataContext="{Binding ElementName=listBoxBooks,Path=SelectedItem,UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Width="200" Content="{Binding}" Click="ButtonBase_OnClick">
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource TextInListTrueFalseConverter}" Mode="OneWay">
<Binding ElementName="listBoxBooks" Path="SelectedItem.KeywordsForTextbox" Mode="OneWay"></Binding>
<Binding RelativeSource="{RelativeSource Self}" Path="Content"></Binding>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
the checkBox.IsChecked multibinding is oneway, and when I click on a checkbox, it calls this method:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
CheckBox cb = (CheckBox)sender;
var content = (string)cb.Content;
var keywords = ((Book)listBoxBooks.SelectedItem).Keywords;
bool clicked = cb.IsChecked.Value;
if (clicked)
keywords.Add(content);
else
keywords.Remove(content);
}
it works more or less but there are 2 caveats:
sometimes the checkbox on which I just clicked is displayed in the combobox's checkbox, which is not expected and is annoying
I have, in addition of the combobox, an other component, a textbox, which contains the list of the keywords for the listview's selectedItem:
but when I click on a checkbox to toogle this, the listbox containing the list is not refreshed...
so I chenged a little my Keywords property, in Book:
public ObservableCollection<string> Keywords
{
get => _keywords;
set
{
_keywords = value;
OnPropertyChanged("Keywords");
OnPropertyChanged("KeywordsForTextbox");
}
}
and the KeywordsForTextbox property is like this:
public string KeywordsForTextbox
{
get { return string.Join(",", _keywords); }
}
finally, to be complete, here is the textBox component in my MainWindow:
<TextBox x:Name="txb_Keywords"
Grid.Column="1"
Width="500"
Text="{Binding ElementName=listBoxBooks,Path=SelectedItem.KeywordsForTextbox,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" />
why does the checkbox appears in the combobox's textbox? why isn't refreshed the other textbox?
thank you.

The problem is that when modifying the Keywords collection the actual Keywords property doesn't change. It's still the same collection object. Only the object's properties (Items) change.
In your Book class you could use methods to do the adding, and removing, then notify property changed from there.
public void AddKeyword(string name)
{
Keywords.Add(name);
OnPropertyChanged("Keywords");
}
public void RemoveKeyword(string name)
{
Keywords.Remove(name);
OnPropertyChanged("Keywords");
}
Then change your event like this.
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
CheckBox cb = (CheckBox)sender;
var content = (string)cb.Content;
var book = ((Book)listBoxBooks.SelectedItem);
bool clicked = cb.IsChecked.Value;
if (clicked)
book.AddKeyword(content);
else
book.RemoveKeyword(content);
}

Related

Select the associated ListViewItem when selecting a control inside the ListView DataTemplate

I have a WinUI 3 ListView that displays a list of items. Every item has a ToggleSwitch and a Expander. When i click on the ToggleSwitch or the Expander the ListView selection does not change.
I found some solutions for WPF but they dont work in WinUI 3:
Selecting a Textbox Item in a Listbox does not change the selected item of the listbox
How can I do this for WinUI 3 so that the associated ListViewItem is selected when the ToggleSwitch or Expander is selected?
You could handle the Tapped event for the Expander and ToggleSwitch and programmatically set the SelectedItem property of the ListView:
private void OnTapped(object sender, TappedRoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
lv.SelectedItem = element.DataContext;
}
XAML:
<ListView x:Name="lv">
<ListView.ItemTemplate>
<DataTemplate>
...
<Expander Tapped="OnTapped" ... />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
If you don't want to change selection when you do something programmatically, you can do it this way.
.xaml
<StackPanel>
<Button
Command="{x:Bind ViewModel.TestCommand}"
Content="Click" />
<ListView
x:Name="ListViewControl"
ItemsSource="{x:Bind ViewModel.Items}"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Item">
<StackPanel>
<ToggleSwitch Toggled="ToggleSwitch_Toggled" />
<Expander Expanding="Expander_Expanding" IsExpanded="{x:Bind IsChecked, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
.xaml.cs
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (ViewModel.IsProgrammatical is false)
{
ListViewControl.SelectedItem = (sender as ToggleSwitch)?.DataContext;
}
}
private void Expander_Expanding(Expander sender, ExpanderExpandingEventArgs args)
{
if (ViewModel.IsProgrammatical is false)
{
ListViewControl.SelectedItem = sender.DataContext;
}
}
ViewModel.cs
public partial class Item : ObservableObject
{
[ObservableProperty]
private string text = string.Empty;
[ObservableProperty]
private bool isChecked;
}
public partial class MainWindowViewModel : ObservableObject
{
public bool IsProgrammatical { get; set; }
[ObservableProperty]
private List<Item> items = new()
{
{ new Item() { Text = "A", IsChecked = false,} },
{ new Item() { Text = "B", IsChecked = false,} },
{ new Item() { Text = "C", IsChecked = false,} },
};
[RelayCommand]
private void Test()
{
IsProgrammatical = true;
Items[1].IsChecked = !Items[1].IsChecked;
IsProgrammatical = false;
}
}
Workaround
In this case, the source collection is untouchable and we can't use a flag if the property was changed programmatically or not, we need to use the Tapped event to make the item selected. But unfortunately, the ToggleSwitch's Tapped event is not fired (at least in my environment). Might be a WinUI bug (issue posted here).
As a workaround, at least until this bug gets fixed, you can use the ToggleButton. I tested it out and the Tapped event is fired.
<DataTemplate x:DataType="local:Item">
<StackPanel>
<ToggleButton Tapped="ToggleButton_Tapped" />
<Expander Tapped="Expander_Tapped" IsExpanded="{x:Bind IsChecked, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
private void ToggleButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ListViewControl.SelectedItem = (sender as ToggleSwitch)?.DataContext;
}
private void Expander_Tapped(Expander sender, TappedRoutedEventArgs e)
{
ListViewControl.SelectedItem = sender.DataContext;
}

TextBox.LineCount and TextBox.GetLastVisibleLineIndex() is always -1 when using MVVM pattern on WPF

I have a TabControl in my View, and I dynamically add TabItems which contains a textbox as a content. And when want to get Line Count from Selected Item, it always returns -1, also with textbox.GetLastVisibleLineIndex(). Code is below:
My View:
<TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="385" Margin="5,50,0,0" VerticalAlignment="Top" Width="740" ItemsSource="{Binding Tabs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{ Binding SelectedTab, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBox
Text="{Binding Text, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,diag:PresentationTraceSources.TraceLevel=High }" AcceptsReturn="True" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<cmd:EventToCommand Command="{Binding DataContext.TextChanged, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My ViewModel:
TabItem tabItem = new TabItem();
tabItem.Header = mainModel.Header;
TextBox textBox = new TextBox();
textBox.Text = mainModel.TextFile;
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2);
textBox.LayoutUpdated += (sender3, e3) => textBox_LayoutUpdated(sender3, e3);
tabItem.Content = textBox;
Tabs.Add(tabItem);
SelectedTab = tabItem;
private void textBox_LayoutUpdated(object sender, EventArgs args)
{
lineCount = ((SelectedTab as TabItem).Content as TextBox).LineCount;
}
MainModel is my Model in MVVM.
My View.cs:
this.UpdateLayout();
TabItem tab = this.tabControl.SelectedItem as TabItem;
int index = ((this.tabControl.SelectedItem as TabItem).Content as TextBox).GetLastVisibleLineIndex();
Even here in View.cs is always -1;
I am new in WPF MVVM,
Thanks.
You have many issues with your current code. It seems that you actually missed the point of MVVM.
First of all, a view-model must not be aware of the view, nor any view-specific types (e.g. TabItem). A view-model is just a layer that adopts your model in such way that this model can be presented by the view. A view-model must not construct the view itself, as you do it in your example.
The reason why you get -1 is that the TextBox you add into the tab item will never be laid out, because you override the tab item's ContentTemplate.
There are some other things you're either doing wrong or they're unnecessary:
Binding for the tab control's ItemsSource must not be TwoWay, because the tab control itself will never update this property value. The UpdateSourceTrigger is not needed here for the same reason.
UpdateSourceTrigger is not needed for the SelectedItem binding, because it has by default the PropertyChanged mode
you have a command named TextChanged, but by convention it has to be named TextChangedCommand (with a Command postfix)
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2) is an overkill for an event subscription that creates an unnecessary lambda capturing this, use the method group syntax instead: tabItem.LayoutUpdated += textBox_LayoutUpdated
And now the true MVVM solution:
Suppose you have the item's view-model:
class Item : ViewModelBase
{
public Item(string header, string textFile)
{
Header = header;
this.textFile = textFile;
}
public string Header { get; }
private string textFile;
public string TextFile
{
get => textFile;
set { textFile = value; OnPropertyChanged(); }
}
private int lineCount;
public int LineCount
{
get => lineCount;
set { lineCount = value; OnPropertyChanged(); Debug.WriteLine("Line count is now: " + value); }
}
}
This view-model represents a single item that will be displayed as a tab item. But maybe this will be some other control in the future - actually you don't have to bother with that. The view-model has no idea which controls exactly will display the values. The view-model just provides these values in a convenient way.
So, the Header and the TextFile properties contain the model values. The LineCount property will be calculated by the view (more on that see below).
The main view-model for that would look like this:
class ViewModel : ViewModelBase
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
private Item selectedItem;
public Item SelectedItem
{
get => selectedItem;
set { selectedItem = value; OnPropertyChanged(); }
}
}
Note that the Items collection property is read-only. That means no one can change the reference to the collection, but the collection itself is not read-only. The SelectedItem reference may be updated however.
And now the important point: the LineCount property of the TextBox will also be updated, e.g. when the TextBox wraps the text and the control is resized. So we can't just calculate the lines count in view-model, we need to do this in the view.
However, the view-model, as we know, must not be aware of the view. What to do? In such cases a good developer prefers a Behavior from the System.Windows.Interactivity namespace.
Let's create a simple behavior that will monitor the LineCount of a TextBox:
class LineCountBehavior : Behavior<TextBox>
{
public int LineCount
{
get { return (int)GetValue(LineCountProperty); }
set { SetValue(LineCountProperty, value); }
}
public static readonly DependencyProperty LineCountProperty =
DependencyProperty.Register("LineCount", typeof(int), typeof(LineCountBehavior), new PropertyMetadata(0));
protected override void OnAttached()
{
AssociatedObject.LayoutUpdated += RefreshLineCount;
AssociatedObject.TextChanged += RefreshLineCount;
}
protected override void OnDetaching()
{
AssociatedObject.LayoutUpdated -= RefreshLineCount;
AssociatedObject.TextChanged -= RefreshLineCount;
}
private void RefreshLineCount(object sender, EventArgs e)
{
LineCount = AssociatedObject.LineCount;
}
}
Now we can attach this behavior to any TextBox and use the behavior's LineCount dependency property as a binding source. Here is a full XAML setup:
<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<TabControl.ItemTemplate>
<DataTemplate DataType="local:Item">
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="local:Item">
<TextBox Text="{Binding TextFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" TextWrapping="Wrap">
<i:Interaction.Behaviors>
<local:LineCountBehavior LineCount="{Binding LineCount, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
So this is a 'clean' MVVM solution. Hope I could give you some insights.
BTW, I don't know why you need this line count in the view-model...

Cannot execute Checked event when property change

I have a Combobox that bound a list of Contact defined in that way:
public List<Contact> Contacts { get;set; } = new List<Contact>();
public class Contact
{
public bool IsFavourite { get; set; }
public bool IsChecked { get; set; }
public string Name { get; set; }
}
NB: the Contact class implement INotifyPropertyChange, that I don't wrote in the example.
The Contacts list is bounded on the ComboBox in the following way:
<ComboBox ItemsSource="{Binding Contacts}"
ItemTemplate="{StaticResource CombinedTemplate}" />
where CombinedTemplate contains the following:
<DataTemplate x:Key="NormalItemTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Name}" Checked="Contact_Checked" Unchecked="Contact_Unchecked" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="CombinedTemplate">
<ContentPresenter x:Name="Presenter" Content="{Binding}"
ContentTemplate="{StaticResource NormalItemTemplate}" />
</DataTemplate>
as you can see I bounded the IsChecked of CheckBox to the IsChecked property that is valorized behind code, so suppose that in the Contacts list is added this item:
var contact = new Contact();
contact.IsChecked = true;
contact.IsFavourite = true;
contact.Name = "Foo";
Contacts.Add(contact);
the Checked of the CheckBox should firing automatically 'cause the IsChecked is true, but I doesn't get this working.
What I did wrong? Thanks.
UPDATE
As suggested in the answer for fix this "bug" I should handle the Loaded event, so I did:
<ComboBox ItemsSource="{Binding Contacts}"
Loaded="ContactMenuComboBox_Loaded"
ItemTemplate="{StaticResource CombinedTemplate}" />
in the event I did:
private void ContactMenuComboBox_Loaded(object sender, RoutedEventArgs e)
{
foreach (var contact in Contacts)
{
if (contact.IsFavourite)
{
contact.IsChecked = true;
}
}
}
the IsChecked property is setted correctly, unfortunately the IsChecked event isn't firing.
Forget to say, if I put the code of ContactMenuComboBox_Loaded inside a button, and then press it, well the event IsChecked will firing.
This is a really weird situation.
UPDATE #2
This is the content of Checked event:
private void Contact_Checked(object sender, RoutedEventArgs e)
{
var contact = (sender as CheckBox).DataContext as CheckedListItem<Model.Contact>;
Task.Factory.StartNew(() =>
{
ContactController.GetLeagues(contact);
})
.ContinueWith((prevTask) =>
{
CheckTaskException(prevTask);
});
}
Update
I have checked with WPF and you are indeed right! The first binding evaluation does not fire the Checked event, only the subsequent calls do. This is in contrast to UWP, where the event is fired.
As a workaround, you could handle the Loaded event if you need to perform an action right after the value is first bound. However, if you are implementing the app as MVVM, you might be better off pushing the Checked logic in the setter of the IsChecked property.
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Name}"
Loaded="Contact_Loaded"
Checked="Contact_Checked" Unchecked="Contact_Unchecked" />
And in the Loaded method do something like:
var checkbox = (CheckBox)sender;
if ( checkbox.IsChecked ) Contact_Checked(this, e);
if ( !checkbox.IsChecked ) Contact_UnChecked(this, e);
Update 2
If you just want to execute the actions as soon as possible, you can indeed attach the Loaded event to your window and do the following:
foreach (var contact in Contacts)
{
if (contact.IsFavourite)
{
Task.Factory.StartNew(() =>
{
ContactController.GetLeagues(contact);
})
.ContinueWith((prevTask) =>
{
CheckTaskException(prevTask);
});
}
}
Of course to avoid code duplication, you could extract this code into a separate method with a Contact parameter.
Original answer
The problem is that your Contacts property is a normal List. In this case the data-bound controls will not update with any changes to that list after first binding. You should use ObservableCollection<T> instead:
public ObservableCollection<Contact> Contacts { get; set; } =
new ObservableCollection<Contact>();
In addition I don't see a point of having two data templates when one of them contains just a ContentPresenter for the other. You could simplify it into just NormalItemTemplate:
<ComboBox ItemsSource="{Binding Contacts}"
ItemTemplate="{StaticResource NormalItemTemplate}" />
Finally, the data-binding for Name is not correct, the Item. prefix should not be there:
<DataTemplate x:Key="NormalItemTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Name}"
Checked="Contact_Checked" Unchecked="Contact_Unchecked" />
</StackPanel>
</DataTemplate>

Find Control in DataTemplate with a multi selection listbox

I have a multi selection listbox where a user can tick multiple items in the list. At the moment I have it so when a checkbox is ticked the ListBoxItem it is within also gets selected:
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
//Select the Item using the DataContext of the Button
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = LstDistro.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
Now I am trying to do it the other way. Whenever a ListBoxItem is selected the checkbox within it gets ticked. So far I have it so the first item you select will get ticked but after that none of the other items you select get ticked. I need to somehow have it loop through all the current selected items.
My current code:
WPF:
<ListBox x:Name="LstDistro" HorizontalAlignment="Left" Margin="10,10,0,42" Width="235" BorderBrush="Black" BorderThickness="2,2,1,1" SelectionMode="Multiple" SelectionChanged="LstDistro_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Canvas x:Name="EventItem" HorizontalAlignment="Left" Height="42" VerticalAlignment="Top" Width="215" Background="{Binding Path=LBackground}">
<Label Content="{Binding LInits}" HorizontalAlignment="Left" Height="33" VerticalAlignment="Top" Width="40" FontWeight="Bold" FontSize="12" Canvas.Top="5"/>
<Label Content="{Binding LFullName}" HorizontalAlignment="Left" Height="33" VerticalAlignment="Top" Width="164" FontSize="12" Canvas.Left="40" Canvas.Top="5"/>
<CheckBox x:Name="ChkName" Height="20" Width="20" Canvas.Left="190" Canvas.Top="12" Checked="CheckBox_Checked" IsChecked="False"/>
</Canvas>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
C#:
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
//Select the Item using the DataContext of the Button
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = LstDistro.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
private void LstDistro_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//Get the current selected item
ListBoxItem item = LstDistro.ItemContainerGenerator.ContainerFromIndex(LstDistro.SelectedIndex) as ListBoxItem;
CheckBox ChkName = null;
//Get the item's template parent
ContentPresenter templateParent = GetFrameworkElementByName<ContentPresenter>(item);
//Get the DataTemplate that the Checkbox is in.
DataTemplate dataTemplate = LstDistro.ItemTemplate;
ChkName = dataTemplate.FindName("ChkName", templateParent) as CheckBox;
ChkName.IsChecked = true;
}
private static T GetFrameworkElementByName<T>(FrameworkElement referenceElement) where T : FrameworkElement
{
FrameworkElement child = null;
for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceElement); i++)
{
child = VisualTreeHelper.GetChild(referenceElement, i) as FrameworkElement;
System.Diagnostics.Debug.WriteLine(child);
if (child != null && child.GetType() == typeof(T))
{ break; }
else if (child != null)
{
child = GetFrameworkElementByName<T>(child);
if (child != null && child.GetType() == typeof(T))
{
break;
}
}
}
return child as T;
}
You should set the Checkbox IsChecked binding like this:
IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"
This means whenever you select a ListBoxItem your checkbox will also become checked.
Hope this helped :)
See the VisualHelper class here
It provides extension methods FindVisualChild, FindVisualChilds
var checkedCheckBoxes= LstDistro.FindVisualChilds<CheckBox>().Where(s=>s.IsChecked==true);
Ok. Delete all that code and start all over.
If you're working with WPF, you really need to understand and embrace The WPF Mentality.
This is how you do what you're looking for, in proper WPF:
<Window x:Class="WpfApplication14.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication14"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<Button Content="Show Selected Item Count" Click="Button_Click"
DockPanel.Dock="Top"/>
<Button Content="Select All" Click="SelectAll"
DockPanel.Dock="Top"/>
<Button Content="Select All" Click="UnSelectAll"
DockPanel.Dock="Top"/>
<ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding DisplayName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</DockPanel>
</Window>
Code behind:
public partial class MainWindow : Window
{
private ObservableCollection<SelectableItem> Items { get; set; }
public MainWindow()
{
InitializeComponent();
//Create dummy items, you will not need this, it's just part of the example.
var dummyitems = Enumerable.Range(0, 100)
.Select(x => new SelectableItem()
{
DisplayName = x.ToString()
});
DataContext = Items = new ObservableCollection<SelectableItem>(dummyitems);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(Items.Count(x => x.IsSelected).ToString());
}
private void SelectAll(object sender, RoutedEventArgs e)
{
foreach (var item in Items)
item.IsSelected = true;
}
private void UnSelectAll(object sender, RoutedEventArgs e)
{
foreach (var item in Items)
item.IsSelected = false;
}
}
Data Item:
public class SelectableItem:INotifyPropertyChanged
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
public string DisplayName { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Result:
Notice How simple and happy life is when you're using WPF's capabilities instead of a manual, procedural, winforms-like approach.
Simple, simple properties and INotifyPropertyChanged. That's how you program in WPF. No need for complicated VisualTreeHelper.Whatever() stuff, no need to manipulate the UI in procedural code. Just simple, beautiful DataBinding.
See how I'm operating against my Data in the SelectAll() and UnSelectAll() methods, rather than the UI. The UI is not responsible for maintaining the state of data, only for showing it.
Copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.
WPF Rocks

Attached Behavior to execute command for ListViewItem

I am trying to use an attached behavior to execute a command in my ViewModel when the user Double Clicks on the list item.
I have reviewed a number of articles on the subject, and tried creating a simple test application but am still having problems eg.
Firing a double click event from a WPF ListView item using MVVM
My simple test ViewModel has 2 collections, one that returns a list of strings and the other that returns a List of ListViewItem types
public class ViewModel
{
public ViewModel()
{
Stuff = new ObservableCollection<ListViewItem>
{
new ListViewItem { Content = "item 1" },
new ListViewItem { Content = "item 2" }
};
StringStuff = new ObservableCollection<string> { "item 1", "item 2" };
}
public ObservableCollection<ListViewItem> Stuff { get; set; }
public ObservableCollection<string> StringStuff { get; set; }
public ICommand Foo
{
get
{
return new DelegateCommand(this.DoSomeAction);
}
}
private void DoSomeAction()
{
MessageBox.Show("Command Triggered");
}
}
Here is the attached property which is like may other examples you see:
public class ClickBehavior
{
public static DependencyProperty DoubleClickCommandProperty = DependencyProperty.RegisterAttached("DoubleClick",
typeof(ICommand),
typeof(ClickBehavior),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ClickBehavior.DoubleClickChanged)));
public static void SetDoubleClick(DependencyObject target, ICommand value)
{
target.SetValue(ClickBehavior.DoubleClickCommandProperty, value);
}
public static ICommand GetDoubleClick(DependencyObject target)
{
return (ICommand)target.GetValue(DoubleClickCommandProperty);
}
private static void DoubleClickChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
ListViewItem element = target as ListViewItem;
if (element != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
element.MouseDoubleClick += element_MouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.MouseDoubleClick -= element_MouseDoubleClick;
}
}
}
static void element_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(ClickBehavior.DoubleClickCommandProperty);
command.Execute(null);
}
}
In my main window, I have defined the style which sets the attached behaviour and binds to the Foo command
<Window.Resources>
<Style x:Key="listViewItemStyle" TargetType="{x:Type ListViewItem}">
<Setter Property="local:ClickBehavior.DoubleClick" Value="{Binding Foo}"/>
</Style>
</Window.Resources>
Works fine when ListViewItems are defined:
<!-- Works -->
<Label Grid.Row="2" Content="DoubleClick click behaviour:"/>
<ListView Grid.Row="2" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}">
<ListViewItem Content="Item 3" />
<ListViewItem Content="Item 4" />
</ListView>
This works too, when bound to the list of type ListViewItem:
<!-- Works when items bound are of type ListViewItem -->
<Label Grid.Row="3" Content="DoubleClick when bound to ListViewItem:"/>
<ListView Grid.Row="3" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}" ItemsSource="{Binding Stuff}">
</ListView>
But this doesn't:
<!-- Does not work when items bound are not ListViewItem -->
<Label Grid.Row="4" Content="DoubleClick when bound to string list:"/>
<ListView Grid.Row="4" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}" ItemsSource="{Binding StringStuff}">
</ListView>
In the output window you see the error, but finding it difficult to understand what is wrong.
System.Windows.Data Error: 39 : BindingExpression path error: 'Foo' property not found on 'object' ''String' (HashCode=785742638)'. BindingExpression:Path=Foo; DataItem='String' (HashCode=785742638); target element is 'ListViewItem' (Name=''); target property is 'DoubleClick' (type 'ICommand')
So my quesion is: How can you get the Command wired up correctly to each ListViewItem when you bind your ListView to a list of Model objects?
Thanks.
The problem is that the DataContext for the Binding is the string. Since there is no Foo property of the string class, you are getting an error. This doesn't happen in the other cases because they inherit their DataContext from the parent (this doesn't happen for automatically generated containers for data items - their DataContext is the data item).
If you change your binding to use the parent ListView's DataContext, it should work fine:
Value="{Binding DataContext.Foo, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"

Categories