Bindings on child dependency object of usercontrol not working - c#

I'm trying to get a binding to work on a child object of a user control. The Xaml looks like this:
<MyGrid>
<MyColumn ExtendedColumnData="{Binding ColumnToolTipDescriptions}"/>
</MyGrid>
Here is how the classes are defined:
[ContentProperty("Columns")]
public class MyGrid : UserControl
{
private MyColumnCollection _columns;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Category("Data")]
public MyColumnCollection Columns
{
get
{
if (_columns == null)
_columns = new MyColumnCollection();
return _columns;
}
}
}
public class MyColumnCollection : ObservableCollection<MyGridColumn>
{
}
public class MyGridColumn : DependencyObject
{
public object ExtendedColumnData
{
get { return (object)GetValue(ExtendedColumnDataProperty); }
set { SetValue(ExtendedColumnDataProperty, value); }
}
public static readonly DependencyProperty ExtendedColumnDataProperty =
DependencyProperty.Register("ExtendedColumnData", typeof(object), typeof(MyGridColumn), new UIPropertyMetadata(null));
}
From what I can tell, the binding is not even attempting to get the data as I've tried putting a converter against the binding, and the breakpoint on the Convert method never gets hit.
I'm using the MVVM pattern so the window's DataContext property is set to a view model.
I've read some other questions on here and tried various permutations of the binding such as:
<MyColumn ExtendedColumnData="{Binding DataContext.ColumnToolTipDescriptions, ElementName=MyViewName}" />
<MyColumn ExtendedColumnData="{Binding DataContext.ColumnToolTipDescriptions, RelativeSource={RelativeSource AncestorType={x:Type local:MyView}}" />
But still no luck, the binding doesn't fire! The annoying thing is, this seems to work fine (if I add the property to the grid):
<MyGrid ExtendedColumnData="{Binding ColumnToolTipDescriptions}">
<MyColumn />
</MyGrid>
I'm not that experienced with WPF so I'm sure I'm missing something?

The problem is that MyColumnCollection is not inheriting data context (usual properties of a control are not part of inheritance context). If you don't have a data context bindings will not work.
To fix that, try inheriting MyColumnCollection not from ObservableCollection, but from FreezableCollection (freezable properties are part of inheritance context).

The problem is logical tree. The direct solution should be (adapted to your example):
public MyColumnCollection Columns
{
get
{
if (_columns == null)
{
_columns = new MyColumnCollection();
_columns.CollectionChanged += Columns_CollectionChanged;
}
return _columns;
}
}
private void Columns_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (var item in e.NewItems)
{
AddLogicalChild(item);
}
}
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
RemoveLogicalChild(item);
}
}
// not sure about Action == reset
}
protected override IEnumerator LogicalChildren
{
get
{
return _columns == null ? null : _columns.GetEnumerator();
}
}
to maintain the logical tree of controls in order for dataContext to be propagated to children. When you call AddLogicalChild, it marks MyGrid as containing logical children, then LogicalChildren will be read and the dataContext of those children will be set (you can listen to DataContextChanged event in them). Overriding LogicalChildren is essential because FrameworkElement doesn't keep the list of children through AddLogicalChild and RemoveLogicalChild calls, oddly.

Related

Using different instances of a ViewModel class can cause problems updating an ObservableCollection?

I have a BookViewModel class with some properties, one of then an ObservableCollection. But I'm having problems updating its value. This is my case:
public class BookViewModel : INotifyPropertyChanged
{
private IEnumerable<Book> booksList;
private ObservableCollection<Chapter> selectedChapters = new ObservableCollection<Chapter>();
public BookViewModel()
{
}
public BookViewModel(List<Book> booksList)
{
this.BooksList = booksList;
}
// ...
public ObservableCollection<Book> SelectedChapters
{
get
{
return this.selectedChapters;
}
set
{
this.selectedChapters = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In one UserControl of my application I do:
private TrainViewModel booksViewModel;
// ...
booksViewModel = new BookViewModel(booksList); // booksList comes from other site
this.DataContext = this.booksViewModel;
And in another UserControl, which is created dynamically as a child of the previous UserControl, I do:
private TrainViewModel booksViewModel;
// ...
booksViewModel = new BookViewModel();
this.DataContext = this.booksViewModel; // Different constructor
In this latter page, I have some checkboxes which modify my selectedChapters property by adding or removing elements from it:
// When some checkbox is checked
this.booksViewModel.SelectedChapters.Add(selectedChapter);
// When some checkbox is unchecked
this.booksViewModel.SelectedChapters.Remove(selectedChapter);
If each time a checkbox is checked or unchecked I do:
Debug.Print(this.booksViewModel.SelectedChapters.Count()); // Always print out 1!!
I'm wondering if using the same ViewModel, but with different instances in each view (the new thing), is causing the problem.
Ok, I could solve it. Not sure if I'm expressing myself well, but it was like I was modifying different sources of data (i.e. data contexts). So, what I did was try to cast the data context of the child UserControl to a BookViewModel (which is the data context of its parent) and work from that:
// Inside the event handler for check and uncheck
BookViewModel bookViewModel = this.DataContext as BookViewModel;
// When some checkbox is checked
if (bookViewModel != null){
this.booksViewModel.SelectedChapters.Add(selectedChapter);
}
// When some checkbox is unchecked
if (bookViewModel != null){
this.booksViewModel.SelectedChapters.Remove(selectedChapter);
}
And that's all. Update perfectly. I don't do anything with related to data dontext or view model in any part of the code (even in the constructor). Now, it's like I'm modifying the data in the same data context of the parent (sorry if my explanation isn't precise, I'm still getting used to WPF concepts).

Xamarin forms C#: Cast IEnumerable to ObservableCollection<>

I'm creating a replacement for ListView that allows the user to rearrange items in the UI.
But if the screen that is using this new ListView wants to know when items are rearranged, it will set the ItemsSource property to an ObservableCollection and then expect to be told when items are rearranged.
XAML:
<Layout>
<MyListView x:Name="MyList">
</Layout>
Code behind:
public class MyScreen
{
ObservableCollection<MyItem> Items = new ObservableCollection<MyItem>();
public MyScreen()
{
InitializeComponent();
MyList.ItemsSource = Items;
Items.CollectionChanged += OnItemsChanged;
}
}
MyListView.cs:
public class MyListView : AbsoluteLayout
{
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(MyListView), null);
public IEnumerable ItemsSource
{
get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
set { this.SetValue(ItemsSourceProperty, value); }
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (propertyName == nameof(ItemsSource))
{
foreach (object item in ItemsSource)
{
// Create a visual element, and set the BindingContext of that
// visual element to 'item'
}
}
}
public void RearrangeItem(int beforeIndex, int afterIndex)
{
if (ItemsSource is ObservableCollection<ARGH> collection)
{
collection.RemoveAt(...);
collection.Insert(...);
}
}
}
And you see the problem. I can't cast ItemsSource (an IEnumerable for consistency with Xamarin.Forms.ListView) to an ObservableCollection without knowing the type of objects in the collection, and the most straightforward way to do that would be to have the MyListView be generic as well, but since I'm creating the MyListView in XAML I can't do that.
I'm sure I could do some clever stuff with reflection where I ask the ItemsSource whether it derives from any ObservableCollection, and if so find the "RemoveAt" and "Insert" functions and call them using reflection, but I was hoping to find something simpler ...
ObservableCollection<T> implements the non-generic IList interface, so you can cast to that and call RemoveAt() & Insert() passing object arounds.
SLaks answer is the easiest if all you need is RemoveAt() and Insert(); for Move() this is what I ended up with so that I could call ObservableCollection<>.Move(oldIndex, newIndex) on an IEnumerable:
Type t = ItemsSource.GetType();
if (t.Name == "ObservableCollection`1")
{
MethodInfo method = t.GetRuntimeMethod("Move", new Type[] { typeof(Int32), typeof(Int32) });
method.Invoke(ItemsSource, new object[] { oldIndex, newIndex });
}

Accessing ItemsSource from GridView

I have a button, When it's clicked it populates my Datagrid. The code is written within the .xaml.cs file, which I believe breaks the MVVM rule but it's just a temporary situation. I know it's not ideal for MVVM.
Calculate.xaml.cs
public void PopulateGrid(object sender, RoutedEventArgs e)
{
BindableCollection<Payments> PaymentCollection = new BindableCollection<Payments>
....
Datagrid.ItemsSource = PaymentCollection
....
}
My question is if there's a way to read the Datagrids ItemsSource From the ViewModel.
What I've Tried
LoansViewModel
public BindableCollection<Payments> paymentCollection {get; set;}
Calculate.xaml
<telerik:RadGridView ItemsSource="{Binding paymentCollection, Mode=TwoWay}" ... />
The collection paymentCollection Doesn't Update after calculate is clicked.
Just do this the correct MVVM way. Get rid of your PopulateGrid method in the .xaml.cs file and eliminate setting the Click property in your xaml. Instead bind the command property of the button to an ICommand property in your ViewModel the same way you are binding the ItemsSource of the RadGridView. You will need an implementation of ICommand to use and MVVM Lights RelayCommand is one option for that.
Here is the code for the ICommand:
private ICommand _populateGridCommand;
public ICommand PopulateGridCommand
{
get
{
if (_populateGridCommand == null)
{
_populateGridCommand = new RelayCommand(() => PopulateGrid());
}
return _populateGridCommand;
}
}
public void PopulateGrid()
{
PaymentCollection.Clear();
//load data and then add to the collection
}
UPDATE
To do this in code behind, you'll need to access the ViewModel and work on the collection from it. I don't like this but it should work.
public void PopulateGrid(object sender, RoutedEventArgs e)
{
var loansVM = DataGrid.DataContext as LoansViewModel;
loansVM.paymentsCollection.Clear();
var newData = //load data
foreach (var data in newData)
loansVM.paymentsCollection.Add(data);
}
Your xaml code looks like it should work provided the DataContext of your grid is set to your ViewModel instance where your paymentCollection property is declared.
Once your binding is set, it calls the get on the paymentCollection property. If your collection property object is not reassigned any further, and you add and remove elements from it, and it notifies on those changes via INotifyCollectionChanged, it will work. This is how ObservableCollection works and used most commonly for such scenarios.
However, if when you calculate, you re-assign your paymentCollection property with a new instance, your grid will not update, because you now have an entirely different collection. In that case you will need to notify the view that the paymentCollection property itself has changed. In which case you should implement it as a notification property:
private BindableCollection<Payments>_paymentCollection;
public BindableCollection<Payments> paymentCollection {
get { return _paymentCollection; }
set {
_paymentCollection = value;
OnPropertyChanged("paymentCollection");
}
}
protected void OnPropertyChanged(string name) {
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null) {
handler(this, new PropertyChangedEventArgs(name));
}
}

Master/detail view using TreeView

I'm working on implementing a master/details view in my application using a TreeView and a custom details view control. I'm also trying to stick to the MVVM pattern.
Right now the TreeView is bound to a collection of view model objects that contain all of the details and the details view is bound to the selected item of the TreeView.
This works great... until one of the TreeView nodes has 5,000 children and the application is suddenly taking up 500MB of RAM.
Main window view model:
public class MainWindowViewModel
{
private readonly List<ItemViewModel> rootItems;
public List<ItemViewModel> RootItems { get { return rootItems; } } // TreeView is bound to this property.
public MainWindowViewModel()
{
rootItems = GetRootItems();
}
// ...
}
Item view model:
public ItemViewModel
{
private readonly ModelItem item; // Has a TON of properties
private readonly List<ItemViewModel> children;
public List<ItemViewModel> Children { get { return children; } }
// ...
}
Here's how I'm binding the details view:
<View:ItemDetails DataContext="{Binding SelectedItem, ElementName=ItemTreeView}" />
I'm fairly new to WPF and the MVVM pattern, but it seems like a waste to I want to bind the TreeView to a collection of a smaller, simplified object that only has properties necessary for displaying the item (like Name and ID), then once it is selected have all of the details loaded. How would I go about doing something like this?
Overview
This should be a simple matter of binding the TreeView's selected item property to something on your source. However, because of the way the TreeView control was built, you have to write more code to get an MVVM-friendly solution, using out-of-the-box WPF.
If you're using vanilla WPF (which I'm assuming you are), then I'd recommend going with an attached behavior. The attached behavior would bind to an action on your main view model that would be invoked when the TreeView's selection changes. You could also invoke a command instead of an action, but I'm going to show you how to use an action.
Basically, the overall idea is to use one instance of your details view model that will be made available as a property of your master view model. Then, instead of your RootItems collection having hundreds of instances of view models, you can use light-weight objects that simply have a display name for the node and perhaps some kind of id field behind them. When the selection on your TreeView changes, you want to notify your details view model by either calling a method or setting a property. In the demonstration code below, I'm setting a property on the DetailsViewModel called Selection.
Walkthrough with Code
Here's the code for the attached behavior:
public static class TreeViewBehavior
{
public static readonly DependencyProperty SelectionChangedActionProperty =
DependencyProperty.RegisterAttached("SelectionChangedAction", typeof (Action<object>), typeof (TreeViewBehavior), new PropertyMetadata(default(Action), OnSelectionChangedActionChanged));
private static void OnSelectionChangedActionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var treeView = sender as TreeView;
if (treeView == null) return;
var action = GetSelectionChangedAction(treeView);
if (action != null)
{
// Remove the next line if you don't want to invoke immediately.
InvokeSelectionChangedAction(treeView);
treeView.SelectedItemChanged += TreeViewOnSelectedItemChanged;
}
else
{
treeView.SelectedItemChanged -= TreeViewOnSelectedItemChanged;
}
}
private static void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var treeView = sender as TreeView;
if (treeView == null) return;
InvokeSelectionChangedAction(treeView);
}
private static void InvokeSelectionChangedAction(TreeView treeView)
{
var action = GetSelectionChangedAction(treeView);
if (action == null) return;
var selectedItem = treeView.GetValue(TreeView.SelectedItemProperty);
action(selectedItem);
}
public static void SetSelectionChangedAction(TreeView treeView, Action<object> value)
{
treeView.SetValue(SelectionChangedActionProperty, value);
}
public static Action<object> GetSelectionChangedAction(TreeView treeView)
{
return (Action<object>) treeView.GetValue(SelectionChangedActionProperty);
}
}
Then, in the XAML on your TreeView element, apply the following: local:TreeViewBehavior.SelectionChangedAction="{Binding Path=SelectionChangedAction}". Note that you will have to substitute local for the namespace of the TreeViewBehavior class.
Now, add the following properties to your MainWindowViewModel:
public Action<object> SelectionChangedAction { get; private set; }
public DetailsViewModel DetailsViewModel { get; private set; }
In your MainWindowViewModel's constructor, you need to set the SelectionChangedAction property to something. You might do SelectionChangedAction = item => DetailsViewModel.Selection = item; if your DetailsViewModel has a Selection property on it. That's entirely up to you.
And finally, in your XAML, wire the details view up to its view model like so:
<View:ItemDetails DataContext="{Binding Path=DetailsViewModel}" />
That's the basic architecture of an MVVM friendly solution using straight WPF. Now, with that said, if you're using a framework like Caliburn.Micro or PRISM, your approach would probably be different than what I've provided here. Just keep that in mind.

DependencyProperty binding mode twoway but propertychangedeventhandler is null

I'm trying to follow the MVVM design paradigm with C# and XAML. I'm running into trouble with a nested user control. I'm trying to bind an element on the nested user control to one of the values in the ViewModel (which is bound to the View via the DataContext property). The same ViewModel is used for both the outer and nested user controls.
It partially works as is, but changes only go one-way from the ViewModel to the nested user control. I need the changes made in the nested user control to propagate back to the ViewModel.
Starting with the XAML for the main View, I have:
<UserControl>
<!-- ... -->
<UserControl.DataContext>
<local:MyViewModel x:Name="myViewModel" />
</UserControl.DataContext>
<!-- ... -->
<local:NestedUserControl
x:Name="nestedUserControl"
CustomNestedValue="{Binding Path=CustomValue, ElementName=myViewModel, Mode=TwoWay}" />
</UserControl>
In the C# code for the ViewModel:
// Constructor
public MyViewModel()
{
CustomValue = true;
}
private bool _customValue;
public bool CustomValue
{
get { return _customValue; }
set
{
if (_customValue != value)
{
_customValue = value;
RaisePropertyChanged ("CustomValue");
}
}
}
And in the code behind of the NestedUserControl, I have:
public static readonly DependencyProperty CustomNestedValueProperty =
DependencyProperty.Register (
"CustomNestedValue",
typeof (bool),
typeof (NestedUserControl),
new FrameworkPropertyMetatdata
{
BindsTwoWayByDefault = true,
PropertyChangedCallback =
new PropertyChangedCallback (CustomNestedValueChangedCallback)
});
public bool CustomNestedValue
{
get { return (bool) GetValue (CustomNestedValueProperty); }
set { SetValue (CustomNestedValueProperty, value); }
}
protected static void CustomNestedValueChangedCallback (
DependencyObject Source,
DependencyPropertyChangedEventArgs e)
{
bool value = (bool) e.NewValue;
NestedUserControl control = source as NestedUserControl;
control.OnCustomValueChange (value);
}
public void OnCustomValueChange (bool value)
{
RaisePropertyChanged ("CustomNestedValue");
// Do other stuff ...
}
// This function is where the nested user control gets direct
// interactions from the user which cause the dependency
// property to change. When this event occurs, the change needs
// to be communicated back up to the view model.
private void _onPreviewMouseDown (object sender, MouseButtonEventArgs e)
{
CustomNestedValue = !CustomNestedValue;
}
[Note: Not only do I set the binding mode to TwoWay when setting the binding in XAML, but I attempted to make this the default behavior of the DependencyProperty in the code above. No luck.]
Both the code behind for the nested user control and the ViewModel code contain the below PropertyChangedEventHandler event/response, which is necessary for the INotifyPropertyChanged interface. From what I understand, this is how bindings between XAML elements and the ViewModel are kept in sync.
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
try
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
catch (Exception e)
{
// ...
}
}
When I run the code, whenever the RaisePropertyChanged function is called for the NestedUserControl, the PropertyChanged event is always null. This is only a problem for the nested usercontrol, and not the outer one. Shouldn't this event be automatically set via the binding mechanism?
I've been struggling with this for several days now to no avail. Any help would be much appreciated. Thanks!
Binding to a DependencyObject operates without using the INotifyPropertyChanged interface. In fact, if you set a breakpoint in the getter or setter of the CustomNestedValue property of the NestedUserControl, you'll find it will never hit when binding in XAML. In essence, the INotifyPropertyChanged is a way of achieving binding without descending from DependencyObject.
When the MyViewModel.CustomValue is bound to the NestedUserControl, the binding code calls (in pseudo code):
NestedUserControl.SetBinding(binding, NestedUserControl.CustomNestedValueProperty)
The INotifyPropertyChanged.PropertyChanged event is never registered and will remain null. However, this doesn't necessarily answer why the value isn't going back to the ViewModel.
Regardless, you could remove a few moving pieces and go with
public static readonly DependencyProperty CustomNestedValueProperty =
DependencyProperty.Register("CustomNestedValue",
typeof (bool),
typeof (NestedUserControl),
null);
public bool CustomNestedValue
{
get { return (bool) GetValue (CustomNestedValueProperty); }
set { SetValue (CustomNestedValueProperty, value); }
}
That's how most of my DependencyProperties are written and they do support TwoWay binding.

Categories