Getting null in ViewModel from DependencyProperty after SetValue - c#

I have this extended ListBox with a SelectedItems property to be able to get the selected items from when SelectionMode is set to Extended.
public class BindableMultiSelectListBox : ListBox
{
public static new readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IList), typeof(BindableMultiSelectListBox),
new PropertyMetadata(default(IList)));
public new IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => throw new Exception("This property is read-only. To bind to it you must use 'Mode=OneWayToSource'.");
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetValue(SelectedItemsProperty, base.SelectedItems);
}
}
I use this in a view like this:
<usercontrols:BindableMultiSelectListBox SelectedItems="{Binding SelectedDogs, Mode=OneWayToSource}" ItemsSource="{Binding Dogs}" SelectionMode="Extended"/>
and bind to an ObservableCollection in my viewModel:
private ObservableCollection<string> _selectedDogs;
public ObservableCollection<string> SelectedDogs
{
get
{
return _selectedDogs;
}
set
{
_selectedDogs = value;
OnPropertyChanged(nameof(SelectedDogs));
}
}
When I debug the OnSelectionChanged base.SelectedItems have the selected values as expected, but when the SelectedDogs setter is hit the value is null!
What have I done wrong here?

The target property is supposed to bind to the source property.
Initialize the source collection in the view model:
public ObservableCollection<string> SelectedDogs { get; } = new ObservableCollection<string>();
...and implement the synchronization in the control:
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (SelectedItems != null)
{
foreach (var removed in e.RemovedItems)
SelectedItems.Remove(removed);
foreach (var added in e.AddedItems)
SelectedItems.Add(added);
}
}
XAML:
... SelectedItems="{Binding SelectedDogs}"
If you want to set the source property, you should initialize the collection in the control.

Changing the SelectedDogs property in the viewModel from ObservableCollection<string> to just IList fixed the problem.
It seems to make little difference in practice, but what if I want to be able to specify ObservableCollection in my viewModel? Why doesn't ObservableCollection work? It does inherit from Collection which implements IList.

Related

Xamarin Forms ListView stays empty after filling the source

I have a problem. I created a ListView with as itemsource a List called unknownDeviceList from my ViewModel. Here is my ViewModel:
public class VM_AddDeviceList : BindableObject
{
private List<UnknownDevice> _unknownDeviceList;
public List<UnknownDevice> unknownDeviceList
{
get
{
return _unknownDeviceList;
}
set
{
if (_unknownDeviceList != value)
{
_unknownDeviceList = value;
OnPropertyChanged();
}
}
}
public List<UnknownDevice> deviceList_raw;
public VM_AddDeviceList()
{
deviceList_raw = new List<UnknownDevice>();
unknownDeviceList = new List<UnknownDevice>();
MyHandler();
}
private async Task LoadUnknownDeviceList()
{
deviceList_raw = await App.RestService.GetDevices();
foreach (UnknownDevice device in deviceList_raw)
{
bool containsItem = App.knownDeviceList.Any(item => item.MAC == device.MAC);
if (!containsItem)
{
unknownDeviceList.Add(device);
}
}
}
public Task MyHandler()
{
return LoadUnknownDeviceList();
}
}
Now I can see that unknownDeviceList gets filled in the foreach, but on the screen the ListView stays empty. What am I doing wrong?
Something with the async and await?
You are raising PropertyChanged when setting unknownDeviceList to inform the view that the list has changed. Anyway, there is no way for the view to know that there were items added to unknownDeviceList.
The most idiomatic way to solve the issue would be to use an ObservableCollection<string> instead.
private ObservableCollection<string> _unknownDevices = new ObservableCollection<string>();
public ObservableCollection<string> UnknownDevices => _unknownDevices;
Please note that I've used the expression body syntax for read-only properties for UnknownDevices, it's not a field.
Since ObservableCollection<string> implements INotifyCollectionChanged which can be subscribed to by the binding to UnknownDevices the view is informed about the changes in UnknownDevices and will be updated when any items are added or removed.

TreeView with ObservableCollection Source, no update

I create a simple Treeview that I bound to an ObservableCollection.
ObservableCollection<IMarketDataViewModel> MarketDataItems;
public interface IMarketDataViewModel
{
string Title { get; }
ObservableCollection<IMarketDataViewModel> Items { get; set; }
}
public MarketDataUserControl(IMarketDataViewer viewModel)
{
InitializeComponent();
DataContext = viewModel;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
When I update data in my ViewModel, I only see the first level in my Treeview. The only way I found to resolve the problem is to create an event in my ViewModel and when the data is updated instead calling PropertyChange on MarketDataItems, I trigger the event and the View reset marketDataTreeView.ItemsSource like this :
private void ViewModelOnOnUpdateItems()
{
marketDataTreeView.ItemsSource = null;
marketDataTreeView.ItemsSource = viewModel.MarketDataItems;
}
And this work perfectly --> All levels are displayed.
Someone know why the PropertyChange doesn't work and why I have to reset the ItemsSource ?
I think you should implement a binding to the ItemSource and this is done by a property:
// Create property
public ObservableCollection<IMarketDataViewModel> MarketDataItems { get; private set; }
...
// Create Binding
Binding bindingObject = new Binding("MarketDataItems");
bindingObject.Source = this; //codebehind class instance which has MarketDataItems
marketDataTreeView.SetBinding(TreeView.ItemsSource, bindingObject);
Or the binding in XAML:
<TreeView x:Name="marketDataTreeView" ItemsSource="{Binding Path=MarketDataItems}"/>
Finally the issue is that I didn't call OnPropertyChanged("Items")
public class MarketDataViewModelBase : IMarketDataViewModel, INotifyPropertyChanged
{
.....
private ObservableCollection<IMarketDataViewModel> items;
public ObservableCollection<IMarketDataViewModel> Items
{
get { return items; }
set
{
items = value;
OnPropertyChanged("Items"); //Add this line fix my issue
}
}
}

Filtering a xaml list box

Hi I have a list box with some customer names, I want to filter the list based on the text entered in the TextBox. After researching a bit I heard that we can use CollectionViewSourse and ICollectionView but did not get a stage where I could get it working.
Could you please suggest on how to achieve this.
Your help is much appreciated.
XAML
<TextBox x:Name="txtSearch"/>
<ListBox x:Name="lbCustomers">
XAML.cs
List<string> customerList;
public MainPage()
{
this.InitializeComponent();
customerList = new List<string>();
customerList.Add("Andrew");
customerList.Add("bravo");
customerList.Add("Carol");
customerList.Add("Dela");
customerList.Add("Eva");
customerList.Add("family");
customerList.Add("George");
customerList.Add("Health");
customerList.Add("Illa");
customerList.Add("Jack");
customerList.Add("Andrew");
lbCustomers.ItemsSource = customerList;
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.Source = customerList;
ICollectionView collectionView = collectionViewSource.View;
}
Edit:
I can not access 'CollectionViewSource.GetDefaultView' and 'view.Filter'. I get an error : 'collectionviewsource does not contain a definition for getdefaultview'
When I looked into the definition i did not find the 'GetDefaultView' and 'Filter dependency properties'
public sealed class CollectionViewSource : DependencyObject, ICollectionViewSource
{
public CollectionViewSource();
public static DependencyProperty IsSourceGroupedProperty { get; }
public static DependencyProperty ItemsPathProperty { get; }
public static DependencyProperty SourceProperty { get; }
public static DependencyProperty ViewProperty { get; }
public System.Boolean IsSourceGrouped { get; set; }
public PropertyPath ItemsPath { get; set; }
public System.Object Source { get; set; }
public ICollectionView View { get; }
}
Try getting the default collection view for your collection. Every time your txtSearch changes you have to change the filter.
ICollectionView view = CollectionViewSource.GetDefaultView(customerList);
view.Filter = obj =>
{
string item = obj as string;
return (item.ToLower().Contains(YourFilter));
};
I would advice you to read about data binding and how it use it to bind listboxes and textboxes and manage your collections in your viewmodels.
But to fix your problem as it is.
Define your IcollectionView at a global level just like customerList and in your main change your code to
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.Source = customerList;
collectionView = collectionViewSource.View;
collectionView.Filter = collectionFilter;
lbCustomers.ItemsSource = collectionView;
and add these 2 additional methods
private bool collectionFilter(object obj)
{
if (string.IsNullOrWhiteSpace(txtSearch.Text))
return true;
string name = obj.ToString();
return name.Contains(txtSearch.Text);
}
private void TxtSearch_OnTextChanged(object sender, TextChangedEventArgs e)
{
collectionView.Refresh();
}
change textbox to
<TextBox x:Name="txtSearch" TextChanged="TxtSearch_OnTextChanged"/>
These should be self explanatory changes but if you need any help i am happy to explain
The filter method is where you define your logic of filtering the listbox items on display

WPF ListView scroll from view model

I have observable collection called (Users) in view model that binded with ListViewControl (lstUsers) in view and what I need is to scroll to current logged in user in List View .
I see in most of examples that used scroll from code behind as following e.g. :
lstUsers.ScrollIntoView(lstUsers[5]);
but what I need is to handle it from view model .
Please advice !
One way of doing this would be to use something like an ICollectionView which has a current item. You can then set IsSynchronizedWithCurrentItem to true to link the current item in the view model to the selected item in the ListView.
Finally handle the event SelectionChanged in the code behind the view to change the scroll position so that it always displays the selected item.
For me the benefit of this method is that the viewmodel is kept unaware of anything about the view which is one of the aims of MVVM. The code behind the view is the perfect place for any code concerning the view only.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView x:Name="View"
SelectionChanged="Selector_OnSelectionChanged" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Items}"/>
<Button Grid.Row="1" Command="{Binding ChangeSelectionCommand}">Set</Button>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
View.ScrollIntoView(View.SelectedItem);
}
}
public class ViewModel
{
private readonly CollectionViewSource _source = new CollectionViewSource();
public ICollectionView Items
{
get { return _source.View; }
}
public ICommand ChangeSelectionCommand { get; set; }
public ViewModel()
{
SetUp();
ChangeSelectionCommand = new Command(ChangeSelection);
}
private void SetUp()
{
var list = new List<string>();
for (int i = 0; i < 100; i++)
{
list.Add(i.ToString(CultureInfo.InvariantCulture));
}
_source.Source = list;
}
private void ChangeSelection()
{
var random = new Random(DateTime.Now.Millisecond);
var n = random.Next(100);
Items.MoveCurrentToPosition(n);
}
}
public class Command : ICommand
{
private readonly Action _action;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action();
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
_action = action;
}
}
let me share my solution with you
Create your own ListView descendant with dependency property TargetListItem
public class ScrollableListView : ListView
{
/// <summary>
/// Set this property to make ListView scroll to it
/// </summary>
public object TargetListItem
{
get { return (object)GetValue(TargetListItemProperty); }
set { SetValue(TargetListItemProperty, value); }
}
public static readonly DependencyProperty TargetListItemProperty = DependencyProperty.Register(
nameof(TargetListItem), typeof(object), typeof(ScrollableListView), new PropertyMetadata(null, TargetListItemPropertyChangedCallback));
static void TargetListItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var owner = (ScrollableListView)d;
owner.ScrollToItem(e.NewValue);
}
public void ScrollToItem(object value)
{
if (value != null && Items != null && Items.Contains(value))
{
ScrollIntoView(value);
}
}
}
create property in ViewModel
object currentListItem;
public object СurrentListItem
{
get => сurrentListItem;
set
{
if (сurrentListItem != value)
{
сurrentListItem = value;
OnPropertyChanged(nameof(СurrentListItem));
}
}
}
bind it
<controls:ScrollableListView ... TargetListItem="{Binding CurrentListItem}"/>
Now you can set CurrentListItem in ViewModel when needed. And the corresponding visual element will become visible in the ListView immediately.
Also maybe you just can use attached property on ListView instead of creating ScrollableListView. But i'm not sure.
Yep, there's always times in MVVM when you need to get at the control. There's various ways of doing this, but here's an easy-ish way of doing it without deriving from the control or messing with routed commands or other such toys what you have in WPF.
In summary:
Create an attached property on your view model.
Set the attached property in XAML to pass the list box back to the view model.
Call .ScrollIntoView on demand.
Note, this is a rough and ready example, make sure your DataContext is set before showing the window.
Code/View Model:
public class ViewModel
{
private ListBox _listBox;
private void ReceiveListBox(ListBox listBox)
{
_listBox = listBox;
}
public static readonly DependencyProperty ListBoxHookProperty = DependencyProperty.RegisterAttached(
"ListBoxHook", typeof (ListBox), typeof (ViewModel), new PropertyMetadata(default(ListBox), ListBoxHookPropertyChangedCallback));
private static void ListBoxHookPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var listBox = (ListBox) dependencyObject;
var viewModel = (ViewModel) listBox.DataContext;
viewModel.ReceiveListBox(listBox);
}
public static void SetListBoxHook(DependencyObject element, ListBox value)
{
element.SetValue(ListBoxHookProperty, value);
}
public static ListBox GetListBoxHook(DependencyObject element)
{
return (ListBox) element.GetValue(ListBoxHookProperty);
}
}
OK, so that will let us get the ListBox passed back to the view; you can do with it as you wish.
Now, just set the property in XAML:
<ListBox wpfApplication1:ViewModel.ListBoxHook="{Binding RelativeSource={RelativeSource Self}}" />
Good to go!

How do you bind ObservableCollections to ItemsSource?

DataContextDataContext context1 = new DataContextDataContext();
public MainWindow()
{
InitializeComponent();
DataContext = new ObservableCollection<MyObject>();
RadGridView1.Filtered+=new EventHandler<GridViewFilteredEventArgs>(RadGridView1_Filtered);
ObservableCollection<MyObject> _MyObject = new ObservableCollection<MyObject>();
foreach (var p in context1.Students)
{
_MyObject.Add(new MyObject { ID = p.StudentID, Name = p.StudentFN });
}
}
void RadGridView1_Filtered(object sender, GridViewFilteredEventArgs e)
{
RadGridView1.ItemsSource = ObservableCollection<MyObject>();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
}
}
public class MyObject
{
public int ID { get; set; }
public string Name { get; set; }
}
How do you bind my ObservableCollections to the ItemsSource?
You want to set the ItemSource to the instance of an ObservableCollection you created in the constructor:
RadGridView1.ItemsSource = _MyObject;
You can make the observable collection as a public property in your code-behind/presenter/viewmodel, like
public ObservableCollection<MyObject> MyObjectCollection {get;set;}
then you can populate that and the binding can be code code behind.
ItemsSource is a dependency property you can bind it in XAML or code behind, like suppose you want to bind to ListBox's(say named lstItems) ItemsSource, like (below code is considering that 'MyObjectCollection' is in codebehind
Binding bindingObject = new Binding("MyObjectCollection");
bindingObject.Source = this; //codebehind class instance which has MyObjectCollection
lstItems.SetBinding(ListBox.ItemsSource, bindingObject);
or in XAML,
<ListBox x:Name="lstItems" ItemsSource="{Binding Path=MyObjectCollection}"/>
for both the ways above you need to set the datacontext which is 'this' (for this specific solution).
But maybe you want to look into basic WPF databinding where you can understand Depedency properties, binding objects, binding modes, etc.
http://msdn.microsoft.com/en-us/library/aa480224.aspx
http://msdn.microsoft.com/en-us/library/ms750612.aspx
http://joshsmithonwpf.wordpress.com/2008/05/19/gradual-introduction-to-wpf-data-binding/

Categories

Resources