I have implemented a selection pattern similar to the one described in this post using a ViewModel to store the IsSelected value, and by binding the ListViewItem.IsSelected to the ViewModel IsSelected:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListView.ItemContainerStyle>
It works in general, but I encounter a severe problem. By using the a VirtualizingStackPanel as the panel in the list view, only the visible ListViewItem are getting created. If I use "Ctrl+A" to select all items, or by using shortcut combination such "Shift+Ctrl+End" on the first item, all items get selected, but for the non visible items, the ViewModel does not get its IsSelected set to true. That is logical, because if the ListViewItem are not created, the binding can't work.
Anybody experienced the same issue, and found a solution (apart from not using a VirtualizingStackPanel)?
I found another way of handling selection in the MVVM pattern, which solved my issue. Instead of maintaining the selection in the viewmodel, the selection is retrieved from the ListView/ListBox, and passed as a parameter to the Command. All done in XAML:
<ListView
x:Name="_items"
ItemsSource="{Binding Items}" ... />
<Button
Content="Remove Selected"
Command="{Binding RemoveSelectedItemsCommand}"
CommandParameter="{Binding ElementName=_items, Path=SelectedItems}"/>
in my ViewModel:
private void RemoveSelection(object parameter)
{
IList selection = (IList)parameter;
...
}
In my case, I ended up solving this by deriving a ListBoxEx class from ListBox, and adding code to respond to selection changes, enforcing the selection state on the item view models:
private readonly List<IListItemViewModelBase> selectedItems = new List<IListItemViewModelBase>();
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
bool isVirtualizing = VirtualizingStackPanel.GetIsVirtualizing(this);
bool isMultiSelect = (SelectionMode != SelectionMode.Single);
if (isVirtualizing && isMultiSelect)
{
var newSelectedItems = SelectedItems.Cast<IListItemViewModelBase>();
foreach (var deselectedItem in this.selectedItems.Except(newSelectedItems))
{
deselectedItem.IsSelected = false;
}
this.selectedItems.Clear();
this.selectedItems.AddRange(newSelectedItems);
foreach (var newlySelectedItem in this.selectedItems)
{
newlySelectedItem.IsSelected = true;
}
}
}
Apart from not using VirtualizingStackPanel, the only thing I can think of is to capture those keyboard shortcuts and have methods for modifying a certain range of your ViewModel items so that their IsSelected property is set to True (e.g., SelectAll(), SelectFromCurrentToEnd()). Basically bypassing the Binding on ListViewItem for controlling the selection for such cases.
Related
I've a collection of items inside an ObservableCollection, each item have a specific nation name (that's only a string). This is my collection:
private ObservableCollection<League> _leagues = new ObservableCollection<League>();
public ObservableCollection<League> Leagues
{
get
{
return _leagues;
}
set
{
_leagues = value;
OnPropertyChanged();
}
}
the League model have only a Name and a NationName properties.
The Xaml looks like this:
<Controls:DropDownButton Content="Leagues" x:Name="LeagueMenu"
ItemsSource="{Binding Leagues}"
ItemTemplate="{StaticResource CombinedTemplate}" >
<Controls:DropDownButton.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding NationName}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</Controls:DropDownButton.GroupStyle>
</Controls:DropDownButton>
but I doesn't get any header for the NationName property, the items inside the DropDown are organized without header but as list, so without organization.
I'm trying to get this predisposition.
What am I doing wrong?
Preliminaries
Grouping items in an ItemsControl in WPF (which DropDownButton derives from) is fairly simple, and is accomplished in two steps. First you need to set up the items source by tweaking an ICollectionView associated with the source collection. Then you need to populate the ItemsControl.GroupStyle collection with at least one GroupStyle item - otherwise the items are presented in a plain (non-grouped) manner.
Diagnosis
The main issue you're facing is getting the drop-down to present the items in a grouped manner. Unfortunately, unlike setting up the items source, it is not something that is easily accomplished in case of the DropDownButton control. The reason for that stems from the way the control (or, more precisely, its template) is designed - the drop-down is presented inside a ContextMenu attached to a Button which is part of the template (see MahApps.Metro source code). Now ContextMenu also derives from ItemsControl, and most of its properties are bound to corresponding properties of the templated DropDownButton. That is however not the case for its GroupStyle property, because it's a read-only non-dependency property, and cannot be bound or event styled. That means that even if you add items to DropDownButton.GroupStyle collection, the ContextMenu.GroupStyle collection remains empty, hence the items are presented in non-grouped manner.
Solution (workaround)
The most reliable, yet most cumbersome solution would be to re-template the control and add GroupStyle items directly to the ContextMenu.GroupStyle collection. But I can offer you a much more concise workaround.
First of all, let's deal with the first step - setting up the items source. The easiest way (in my opinion) is to use CollectionViewSource in XAML. In your case it would boil down to something along these lines:
<mah:DropDownButton>
<mah:DropDownButton.Resources>
<CollectionViewSource x:Key="LeaguesViewSource" Source="{Binding Leagues}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="NationName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</mah:DropDownButton.Resources>
<mah:DropDownButton.ItemsSource>
<Binding Source="{StaticResource LeaguesViewSource}" />
</mah:DropDownButton.ItemsSource>
</mah:DropDownButton>
Now for the main part - the idea is that we'll create a helper class that will contain one attached dependency property that will assign an owner DropDownButton control to the ContextMenu responsible for presenting its items. Upon changing the owner we'll observe its DropDownButton.GroupStyle collection and use ContextMenu.GroupStyleSelector to feed the ContextMenu with items coming from its owner's collection. Here's the code:
public static class DropDownButtonHelper
{
public static readonly DependencyProperty OwnerProperty =
DependencyProperty.RegisterAttached("Owner", typeof(DropDownButton), typeof(DropDownButtonHelper), new PropertyMetadata(OwnerChanged));
public static DropDownButton GetOwner(ContextMenu menu)
{
return (DropDownButton)menu.GetValue(OwnerProperty);
}
public static void SetOwner(ContextMenu menu, DropDownButton value)
{
menu.SetValue(OwnerProperty, value);
}
private static void OwnerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var menu = (ContextMenu)d;
if (e.OldValue != null)
//unsubscribe from the old owner
((DropDownButton)e.OldValue).GroupStyle.CollectionChanged -= menu.OwnerGroupStyleChanged;
if (e.NewValue != null)
{
var button = (DropDownButton)e.NewValue;
//subscribe to new owner
button.GroupStyle.CollectionChanged += menu.OwnerGroupStyleChanged;
menu.GroupStyleSelector = button.SelectGroupStyle;
}
else
menu.GroupStyleSelector = null;
}
private static void OwnerGroupStyleChanged(this ContextMenu menu, object sender, NotifyCollectionChangedEventArgs e)
{
//this method is invoked whenever owners GroupStyle collection is modified,
//so we need to update the GroupStyleSelector
menu.GroupStyleSelector = GetOwner(menu).SelectGroupStyle;
}
private static GroupStyle SelectGroupStyle(this DropDownButton button, CollectionViewGroup group, int level)
{
//we select a proper GroupStyle from the owner's GroupStyle collection
var index = Math.Min(level, button.GroupStyle.Count - 1);
return button.GroupStyle.Any() ? button.GroupStyle[index] : null;
}
}
In order to complete the second step we need to bind the Owner property for the ContextMenu (we'll use DropDownButton.MenuStyle to do that) and add some GroupStyle items to the DropDownButton:
<mah:DropDownButton>
<mah:DropDownButton.MenuStyle>
<Style TargetType="ContextMenu" BasedOn="{StaticResource {x:Type ContextMenu}}">
<Setter Property="local:DropDownButtonHelper.Owner" Value="{Binding RelativeSource={RelativeSource TemplatedParent}}" />
</Style>
</mah:DropDownButton.MenuStyle>
<mah:DropDownButton.GroupStyle>
<GroupStyle />
</mah:DropDownButton.GroupStyle>
</mah:DropDownButton>
This I think should be enough to achieve your goal.
If you check out the other post you've linked to, the answer has it all - in particular you need to bind to a CollectionView, rather than directly to the collection. Then you can set up grouping on the CollectionView.
So, in your case, define the property:
public ICollectionView LeaguesView { get; private set; }
and then after you've created your Leagues Collection, attach the View to your collection, and while you're at it set up the grouping on the view:
LeaguesView = (ListCollectionView)CollectionViewSource.GetDefaultView(Leagues);
LeaguesView.GroupDesriptions.Add(new PropertyGroupDescription("NationName"));
Then, bind your DropDownButton ItemSource to LeaguesView, and change your HeaderTemplate to bind to "Name" - which is the the name of the group:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
You can also use the ItemCount property in there if you want to show how many items there are in the group.
I have a combo box rigged as
<ComboBox x:Name="HeadComboBox"
ItemsSource="{Binding DataContext.HeadList, RelativeSource={RelativeSource FindAncestor,AncestorType= {x:Type views:FixedAssetBaseWholeUC}}}" Margin="195,78,86,0" VerticalAlignment="Top" SelectedItem="{Binding HeadItem}" DisplayMemberPath="Name" />
The datacontext.HeadList will point to:
public List<FixedAssetHeadItem> HeadList
{
get
{
return _headList;
}
set
{
if (_headList != value)
{
_headList = value;
RaisePropertyChanged("HeadList");
}
}
}
I disable the UserControl in which the combobox rests and load another control to edit the items in the headlist by
DeleteFromHeadList(1);
FixedAssetBaseWholeViewModel fbwvm = (FixedAssetBaseWholeViewModel)Fabwuc.DataContext;
fbwvm.HeadList = HeadList;
When the edit is complete the re enable the usercontrol only to find the selection disappers.
Debug shows
http://postimg.org/image/hdz4h4px3/
How should I deal with this?
You should not bind to List (can cause memory leak), but bind to ObservableCollection<> instead. In this way, your ComboBox should update appropriately. Also your HeadItem should be INPC property - in setter (private or public, depends on your code) should be raising of property changes.
In my windows phone 8.1 application I have a listbox when initially loading everything I want to show in the listbox everything is fine. However after scrolling some of the elements will be shown incorrectly. This seems completely random.
The forementioned listbox looks like this in xaml:
<ListBox Name="MainPage_List" Grid.Column="0" Background="#EDEDED" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Components:MyUserControl />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As you can see its datatemplate is linked to a usercontrol. In this UserControl I have a DataContextChanged event. Which looks like this:
private void DataContextChanged(object sender, object e)
{
if (mySource == null)
{
mySource = DataContext as Message;
}
if (mySource != null)
{
if (mySource.Source_Type == SourceTypes.Type1)
{
MyGrid.Visibility = Visibility.Collapsed;
MyOtherGrid.Visibility = Visibility.Visible;
}
else if (mySource.Source_Type == SourceTypes.Type2)
{
MyOtherGrid.Visibility = Visibility.Collapsed;
MyGrid.Visibility = Visibility.Visible;
}
}
}
I check multiple types and variables here and depending on that I set other things visible or load other images. This works fine. However when scrolling through the list sometimes some of the elements will be shown differently than they should. Even when I make sure the code that decides which elements will be shown is not used again.
The source of the list is a custom class enheriting from ObservableCollection using a custom class that enherits from INotifyPropertyChanged.
Does anyone know what I am doing wrong here? Or why this happens and how to get around this?
After some discussion with WereWolfBoy we found the problem.
It is related to the default ItemsPanel of the ListBox which is a VirtualizingStackPanel. It reuses the controls from the ItemTemplate of the ListBox and changes their DataContext when necessary to display different items. Unlike normal StackPanel, it does NOT create separate controls for different items.
For this to work, the items must refresh when the DataContext changes. And here's the actual problem. Because of this code:
if (mySource == null)
{
mySource = DataContext as Message;
}
the DataContext was actually loaded just once, and subsequent changes did not affect the UI. Removing the if and getting the DataContext every time it changes fixed the issue.
This may be a result of virtualization.
If your list is long enough, there is a reuse of controls for items in it.
Imagine you have a list with 1000 items in it, but the listbox only has 30 control instances that are re-used when you scroll up and down.
If this is the case, you will see the wrong behavior repeatin ever X items.
In order to solve this problem, I would recommend that instead of using DataContextChanged, you should expose a property as DependencyProperty.
In your data template, bind to this property and this will do the trick for you.
public bool ShowMyGrid
{
get { return (bool)GetValue(ShowMyGridProperty); }
set { SetValue(ShowMyGridProperty, value); }
}
public static readonly DependencyProperty ShowMyGridProperty =
DependencyProperty.Register("ShowMyGrid", typeof(bool), typeof(MyUserControl1), new UIPropertyMetadata(false, ShowMyGridCallback));
static void ShowMyGridCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myControl = d as MyUserControl1;
bool newVal = (bool)e.NewValue;
if (newVal)
{
myControl.MyGrid.Visibility = Visibility.Collapsed;
myControl.MyOtherGrid.Visibility = Visibility.Visible;
}
else
{
myControl.MyGrid.Visibility = Visibility.Visible;
myControl.MyOtherGrid.Visibility = Visibility.Collapsed;
}
}
Overview
I have an application, that displays data from an observable collection. The observable collection is (in this debugging setting) created and instanciated only once, then the values stay the same.
The main view of the application contains a ListBox that is bound to said observable collection:
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel MinWidth="456" MaxWidth="456" Background="White" Margin="0,0,0,17">
<sparklrControls:SparklrText Post="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Workaround used to stretch the child elements to the full width -> HorizontalContentAlignment won't work for some reason... -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The child items are bound to a UserControl. This UserControl implements a DependancyProperty which the child elements are bound to:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(object), new PropertyMetadata(textPropertyChanged));
private static void postPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SparklrText control = d as SparklrText;
control.Post = (ItemViewModel)e.NewValue;
}
Binding to the post property configures other variables via the getter of the Post property
public ItemViewModel Post
{
get
{
return post;
}
set
{
if (post != value)
{
this.ImageLocation = value.ImageUrl;
this.Username = value.From;
this.Comments = value.CommentCount;
this.Likes = value.LikesCount;
this.Text = value.Message;
post = value;
}
}
}
This setter configures other which in turn set up elements in the user control. Nothing in the user control is bound, the few updates are done with direct access to the respective Content/Text properties. ImageLocation performs an asynchronous download of an image with
private void loadImage(string value)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
image = new BitmapImage();
image.SetSource(e.Result);
MessageImage.Source = image;
};
wc.OpenReadAsync(new Uri(value));
}
Issue
When I scroll down in the list box and back up, the setter of Post is executed when the owning element comes back into view. The problem: value is a different instance of ItemViewModel. The ListBox ItemsSource is not accessed in any way from outside the class. When scrolling back up, it seems like the wrong Items are bound to the elements, resulting in distorted designs. Are there any issues with the Binding that cause this?
The issue was caused by the ListBox. Elements that are scroll out of view are recycled and appended on the other side. In the code above, a asynchronous operation did not check if the result was still valid, causing wrong display data.
I am building a WPF application using C# and also i used MVVM architecture in my application.
I created a CheckBox column in telerik gridview by using DataTemplate. I am using a collection to bind the data in the GridView .
How can i find the particular row number of DataItem has been selected in that Collection When CheckBox is checked on the Grid.
Here My code for creating CheckBox on Grid is:
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<CheckBox Name="StockCheckBox" IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type telerik:GridViewRow}}, Path=IsSelected}" />
</StackPanel>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
And My Collection is,
foreach (var AvailableStock in AvailableStocks)// In this **AvailableStocks**(IEnumurable Collection) I got all the datas in the Gridview
//In this collection How can i know that the particular RowItem is selected in that gridview by CheckBox
{
if (SelectedStock != null)
{
this.SelectedStocks.Add(AvailableStock );
this.RaisePropertyChanged(Member.Of(() => AvailableStocks));
}
}
Anyone Please tell me some suggestion on this How can i achieve this? How can i identify that particular row has been selected?
Thanks in Advance.
I would recommend using an MVVM approach and use a Binding to get the selected items. Unfortunately, the DataGrid doesn't provide a DependencyProperty for selected items, but you can provide your own. Derive a class from DataGrid, register a dependency property for SelectedItems and override the SelectionChanged event to update your dependency property. You can then use a Binding to inform your ViewModel of the selected items.
Code:
public class CustomDataGrid : DataGrid
{
public static readonly DependencyProperty CustomSelectedItemsProperty = DependencyProperty.Register(
"CustomSelectedItems", typeof (List<object>), typeof (CustomDataGrid),
new PropertyMetadata(new List<object>()));
public List<object> CustomSelectedItems
{
get { return (List<object>) GetValue(CustomSelectedItemsProperty); }
set { SetValue(CustomSelectedItemsProperty, value);}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
foreach(var item in e.AddedItems)
CustomSelectedItems.Add(item);
foreach (var item in e.RemovedItems)
CustomSelectedItems.Remove(item);
base.OnSelectionChanged(e);
}
}
XAML:
<Grid>
<Ctrl:CustomDataGrid CustomSelectedItems="{Binding MySelectedItems}"/>
</Grid>