I have a ListBox in my ProWindow which is configured for Extended selection so that the user can select multiple items. I also have a button which is supposed to remove the selected items from the list. I set up the binding for the ListBox as an ObservableCollection. If the Selection mode for the Listbox was Single I would set up a binding for the SelectedItem property of the Listbox. However, when the selection mode is set up for Extended selection I want to use the SelectedItems property, not the SelectedItem property. But I am not sure how to set up the binding in my ViewModel so I can get the IList from the SelectedItems property, which is a read only property. (I don't need to set the SelectedItems, I only need to get it)
I have read that there is a way to pass the SelectedItems property of the ListBox to a Command using a CommandParameters property. It says that the CommandParameter should be declared before the Command binding. The button xaml code is supposed to look something like this:
<ListBox x:Name="lbxAPNList" HorizontalAlignment="Left" Height="143" Margin="10,93,0,0" VerticalAlignment="Top" Width="136" SelectionMode="Extended" ItemsSource="{Binding APNsList}" SelectedItem="{Binding SelectedAPN}"/>
<Button x:Name="btnRemoveFromList" Content="Remove from List" CommandParameter="{Binding ElementName=lbxAPNList, Path=SelectedItems}" Command="{Binding Path=CmdRemoveFromList}"/>
However, I am not sure how to set the ViewModel ICommand declaration up so that I can use that command parameter. I tried the following but it doesn't work, since the code is never triggered:
private ObservableCollection<string> _apnsList = new ObservableCollection<string>();
public ObservableCollection<string> APNsList
{
get { return _apnsList; }
set
{
SetProperty(ref _apnsList, value, () => APNsList);
}
}
private string _selectedAPN;
public string SelectedAPN
{
get { return _selectedAPN; }
set
{
SetProperty(ref _selectedAPN, value, () => SelectedAPN);
}
}
// ListParcels is populated by another button and matches the listbox
public static SortedSet<string> ListParcels { get; set; }
public ICommand CmdRemoveFromList(IList<object> SelectedItems)
{
get
{
return new RelayCommand(() =>
{
Message = "";
// Iterate selected items in APN List
foreach (var item in SelectedItems)
{
// Get item as a string
string _APN = item.ToString();
// Remove the APN from the ListParcels SortedList
ListParcels.Remove(_APN);
}
//Clear the dialog APN list
APNsList.Clear();
// Iterate the APNs in the module ListParcels variable
foreach (string ListParcel in ListParcels)
{
// Add the APNs to the dialog list
APNsList.Add(ListParcel);
}
});
}
}
Is there a way to get the SelectedItems property of a ListBox for use by a button ICommand declaration that works with the Pro SDK?
Solved! Go to Solution.
I got my original code to work by changing the ICommand declaration to this:
public ICommand CmdRemoveFromList
{
get
{
return new RelayCommand((SelectedAPNs) =>
{
// Remove items only when SelectedItem isn't null
if (SelectedAPN != null)
{
// Iterate selected items in APN List
foreach (var item in SelectedAPNs as IList<object>)
{
// Get item as a string
string _APN = item.ToString();
// Remove the APN from the ListParcels variable
ListParcels.Remove(_APN);
}
//Clear the dialog APN list
APNsList.Clear();
// Iterate the APNs in the ListParcels variable
foreach (string ListParcel in ListParcels)
{
// Add the APNs to the dialog list
APNsList.Add(ListParcel);
}
}
}, () => true);
}
}
I used the CommandParameter in the RelayCommand declaration, added the as IList<object> to type cast the CommandParameter as an iterable IList of objects in the foreach loop and added }, () => true); at the end of the RelayCommand to provide a return value for the RelayCommand.
I verified it worked regardless of whether or not the SelectedItems were visible within the current scroll position of the Listbox. So this is the final solution for the xaml, which is nearly identical to the standard MVVM declarations of a Listbox and Button used in Pro, except that I have added a CommandParameter before the button Command.
<ListBox x:Name="lbxAPNList" HorizontalAlignment="Left" Height="143" Margin="10,93,0,0" VerticalAlignment="Top" Width="136" SelectionMode="Extended" ItemsSource="{Binding APNsList}" SelectedItem="{Binding SelectedAPN}"/>
<Button x:Name="btnRemoveFromList" Content="Remove from List" CommandParameter="{Binding ElementName=lbxAPNList, Path=SelectedItems}" Command="{Binding Path=CmdRemoveFromList}"/>
And this is the final code for the ViewModel, which uses the standard MVVM code for the Listbox source and SelecedItem and only makes slight variations in the standard MVVM ICommand declaration for the button.
private ObservableCollection<string> _apnsList = new ObservableCollection<string>();
public ObservableCollection<string> APNsList
{
get { return _apnsList; }
set
{
SetProperty(ref _apnsList, value, () => APNsList);
}
}
private string _selectedAPN;
public string SelectedAPN
{
get { return _selectedAPN; }
set
{
SetProperty(ref _selectedAPN, value, () => SelectedAPN);
}
}
// ListParcels is populated by another button and matches the listbox
// It ensures the list is alphabetically sorted and all values are unique
public static SortedSet<string> ListParcels { get; set; }
public ICommand CmdRemoveFromList
{
get
{
return new RelayCommand((SelectedAPNs) =>
{
// Remove items only when SelectedItem isn't null
if (SelectedAPN != null)
{
// Iterate selected items in APN List
foreach (var item in SelectedAPNs as IList<object>)
{
// Get item as a string
string _APN = item.ToString();
// Remove the APN from the ListParcels variable
ListParcels.Remove(_APN);
}
//Clear the dialog APN list
APNsList.Clear();
// Iterate the APNs in the ListParcels variable
foreach (string ListParcel in ListParcels)
{
// Add the APNs to the dialog list
APNsList.Add(ListParcel);
}
}
}, () => true);
}
}
You can scroll down through this answer: Solved: Detecting Selected Items (Layers) in a List Box vi... - Esri Community, I attached a sample solution to one of the replies that does the listbox with a 'checked' selections and a button to recall all selected (checked) items from the list. The sample is called DockpaneWithListCheckbox. I think there's a way to do it without the checkbox as well, it's a WPF question for which you can find a solution on the internet.
I got my original code to work by changing the ICommand declaration to this:
public ICommand CmdRemoveFromList
{
get
{
return new RelayCommand((SelectedAPNs) =>
{
// Remove items only when SelectedItem isn't null
if (SelectedAPN != null)
{
// Iterate selected items in APN List
foreach (var item in SelectedAPNs as IList<object>)
{
// Get item as a string
string _APN = item.ToString();
// Remove the APN from the ListParcels variable
ListParcels.Remove(_APN);
}
//Clear the dialog APN list
APNsList.Clear();
// Iterate the APNs in the ListParcels variable
foreach (string ListParcel in ListParcels)
{
// Add the APNs to the dialog list
APNsList.Add(ListParcel);
}
}
}, () => true);
}
}
I used the CommandParameter in the RelayCommand declaration, added the as IList<object> to type cast the CommandParameter as an iterable IList of objects in the foreach loop and added }, () => true); at the end of the RelayCommand to provide a return value for the RelayCommand.
I verified it worked regardless of whether or not the SelectedItems were visible within the current scroll position of the Listbox. So this is the final solution for the xaml, which is nearly identical to the standard MVVM declarations of a Listbox and Button used in Pro, except that I have added a CommandParameter before the button Command.
<ListBox x:Name="lbxAPNList" HorizontalAlignment="Left" Height="143" Margin="10,93,0,0" VerticalAlignment="Top" Width="136" SelectionMode="Extended" ItemsSource="{Binding APNsList}" SelectedItem="{Binding SelectedAPN}"/>
<Button x:Name="btnRemoveFromList" Content="Remove from List" CommandParameter="{Binding ElementName=lbxAPNList, Path=SelectedItems}" Command="{Binding Path=CmdRemoveFromList}"/>
And this is the final code for the ViewModel, which uses the standard MVVM code for the Listbox source and SelecedItem and only makes slight variations in the standard MVVM ICommand declaration for the button.
private ObservableCollection<string> _apnsList = new ObservableCollection<string>();
public ObservableCollection<string> APNsList
{
get { return _apnsList; }
set
{
SetProperty(ref _apnsList, value, () => APNsList);
}
}
private string _selectedAPN;
public string SelectedAPN
{
get { return _selectedAPN; }
set
{
SetProperty(ref _selectedAPN, value, () => SelectedAPN);
}
}
// ListParcels is populated by another button and matches the listbox
// It ensures the list is alphabetically sorted and all values are unique
public static SortedSet<string> ListParcels { get; set; }
public ICommand CmdRemoveFromList
{
get
{
return new RelayCommand((SelectedAPNs) =>
{
// Remove items only when SelectedItem isn't null
if (SelectedAPN != null)
{
// Iterate selected items in APN List
foreach (var item in SelectedAPNs as IList<object>)
{
// Get item as a string
string _APN = item.ToString();
// Remove the APN from the ListParcels variable
ListParcels.Remove(_APN);
}
//Clear the dialog APN list
APNsList.Clear();
// Iterate the APNs in the ListParcels variable
foreach (string ListParcel in ListParcels)
{
// Add the APNs to the dialog list
APNsList.Add(ListParcel);
}
}
}, () => true);
}
}
Also, if you search for 'CommandParameter' in our Pro SDK community samples you will find a few implementations that you can look at:
I also figured out how to implement a property that can set the IsSelected property of the ListBoxItems of a ListBox so that it supports a TwoWay update when the Selection Mode is set to Extended and it does not have to be passed as a CommandParameter to an ICommand button. I found that the Extended SelectionMode only mostly worked properly when I included the ListBox parameter for VirtualizingStackPanel.IsVirtualizing="False" along with setting the SelectionMode="Extended". It does not appear to trigger a notification when the user actually extends their multiselection in the ListBox, so as far as I can tell the evaluation or programmatic use of the ListBox IsSelected property has to be triggered by Methods and Actions you design in your code.
In this example code I am presenting a list of Lot numbers so I made my ItemSource binding to an ObservableCollection called Lots. This ObservableCollection contains ListBoxItems based on a custom class called LotSelector. I had to set up an ItemTemplate for the ListBox so the list would display the Item property of the LotSelector custom class in the ListBox and an ItemContainerStyle of ListBoxItems that would assign the IsSelected property of each ListBoxItem to the IsSelected property of my LotSelector custom class. Here is the xaml for the ListBox:
<ListBox x:Name="lbxLots" VirtualizingStackPanel.IsVirtualizing="False" SelectionMode="Extended" ItemsSource="{Binding Lots, Mode=TwoWay}" SelectedItem="{Binding SelectedLot}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Item}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I then added a Class Item to my project named LotSelector and defined two classes in the file. The first class is named LotSelector and stores the values and IsSelected state of each ListBoxItem. The second class is named LotItemComparer based on the IComparer class so that the LotSelector instances could be sorted and kept unique using a SortedSet<LotSelector> ViewModel property. Here is the LotSelector.cs file code:
using System;
using System.Collections.Generic;
using ArcGIS.Desktop.Framework.Contracts;
namespace RivCoSearchForms
{
public class LotSelector : PropertyChangedBase
{
private string _item;
public String Item
{
get { return _item; }
set
{
_item = value;
NotifyPropertyChanged(() => Item);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
NotifyPropertyChanged(() => IsSelected);
}
}
}
public class LotItemComparer : IComparer<LotSelector>
{
public int Compare(LotSelector x, LotSelector y)
{
// TODO: Handle x or y being null, or them not having Items
return x.Item.CompareTo(y.Item);
}
}
}
For the ViewModel I added standard code for my Lots ObservableCollection<LotSelector> property for the ListBox ItemSource and a SelectedLot string property for the SelectedItem property of the ListBox. I also added a SortedSet<LotSelector> private property of the ViewModel to ensure the Lots were distinct and sorted and could be manipulated on the MTC thread. I have included a Command implementation to show how the ListBox could be populated based on the Lot data in a Layer. (I tried to simplify the code of the Command from a more complex version and have not tested it, so the Command code may not run correctly and need correction.) Here is the ViewModel code:
private ObservableCollection<LotSelector> _lots = new ObservableCollection<LotSelector>();
public ObservableCollection<LotSelector> Lots
{
get { return _lots; }
set
{
SetProperty(ref _lots, value, () => Lots);
}
}
private string _selectedLot;
public string SelectedLot
{
get { return _selectedLot; }
set
{
SetProperty(ref _selectedLot, value, () => SelectedLot);
// Blank out Book, Page list and text block
Message = null;
}
}
private SortedSet<LotSelector> ListLotsSelected;
public ICommand CmdCancel => new RelayCommand((proWindow) =>
{
// TODO: set dialog result and close the window
(proWindow as ProWindow).DialogResult = false;
(proWindow as ProWindow).Close();
}, () => true);
public ICommand CmdListLotsInLayer => new RelayCommand(async (proWindow) =>
{
ListLotsSelected = new SortedSet<LotSelector>(new LotItemComparer());
// Get the Active Mapview
var ActMap = MapView.Active;
// Get the search layer from its name
var fLayer = ActMap.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().FirstOrDefault(l => l.Name.Equals("Search Lot Layer"));
// If layer exists proceed
if (fLayer != null)
{
// Set up a Queued Task for map changes
int iCount = await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run(() =>
{
// Execute a Search on the layer
using (RowCursor RecMapCursor = fLayer.Search())
{
int cnt = 0;
// Iterate the found cursor rows
while (RecMapCursor.MoveNext())
{
// Get current row as a feature
using (Feature feature = (Feature)RecMapCursor.Current)
{
// Get the APN value of the feature
LotSelector Lot = new LotSelector();
int number;
bool success = int.TryParse(feature[LotField].ToString(), out number);
if (success)
{
Lot.Item = feature[LotField].ToString().PadLeft(4);
}
else
{
Lot.Item = feature[LotField].ToString();
}
// Set the IsSelected property to select the ListBoxItem
Lot.IsSelected = true;
// Update the ListLotsSelected parameter
ListLotsSelected.Add(Lot);
cnt += 1;
}
}
return cnt;
}
});
if (iCount > 0)
{
Lots.Clear();
foreach (LotSelector _Lot in ListLotsSelected)
{
Lots.Add(_Lot);
}
}
}
}, () => true);