wpf binding upwards: bind to view model property inside nested uiElement - c#

I have a WPF project with a view model and some nested UI elements. Here is the (relevant section of) XAML:
<UserControl> // DataContext is MyVM (set programmatically)
<TreeView ItemsSource="{Binding Trees}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Subtrees}">
<StackPanel>
<ListView ItemsSource="{Binding Contents}"
SelectedValue="{Binding SelectedContent}" // won't work: Tree has no such property
SelectionMode="Single"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</UserControl>
Here the code for the ViewModel class:
public class MyVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public IEnumerable<Tree> Trees { get; set; }
private object _selectedContent;
public string SelectedContent
{
get => _selectedContent;
set
{
_selectedContent = value;
OnPropertyChanged();
}
}
}
Here the code for class Tree:
public class Tree
{
public IEnumerable<Tree> Subtrees { get; set; }
public IEnumerable<string> Contents { get; set; }
}
I want to allow only one selection globally for all ListViews. Just like here, I want to bind all ListViews to the property SelectedContent in the view model MyVM.
The problem is that the data context of the ListView is a Tree, and not the MyVM from the top user control. (It should be Tree, since we want to show the Contents.) I know I can bind downwards using SelectedValuePath, but how do I go up instead in order to bind SelectedValue to the MyVM property SelectedContent?
I tried SelectedValue="{Binding RelativeSource={RelativeSource AncestorType ={x:Type UserControl}}, Path=SelectedContent}", but it did not work.

There is a comment here, saying:
Just wanted to note here that if you want to bind to a property in the
DataContext of the RelativeSource then you must explicitly specify it:
{Binding Path=DataContext.SomeProperty, RelativeSource=.... This was
somewhat unexpected for me as a newbie when I was trying to bind to a
parent's DataContext within a DataTemplate.
This comment deserves more attention, so I'll use it as the correct answer.

Try to use
SelectedValue="{Binding RelativeSource={RelativeSource
Mode=FindAncestorBindingContext, AncestorType ={x:Type MyVM}},
Path=SelectedContent}"
Maybe, you'll need to describe your model's namespace in header:
xmlns:viewmodel="clr-namespace:_your_namespace_.ViewModels"
and use
SelectedValue="{Binding RelativeSource={RelativeSource
Mode=FindAncestorBindingContext, AncestorType ={x:Type viewmodel:MyVM}},
Path=SelectedContent}"

Related

How do I bind the data of a custom UserControl

So I just setup a project and added a custom UserControl that looks like this.
<Grid>
<ItemsControl ItemsSource="{Binding UserViewModel.Users}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<controls:UserCard/>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
As you can see I tried binding the Text property buti it doesn't bind.
Now there could be a lot of reasons to why it's behaving like this so I will try to narrow it down.
I've created a BaseViewModel that will hold my ViewModels and it looks like this.
public class BaseViewModel : ObservableObject
{
public UserViewModel UserViewModel { get; set; } = new UserViewModel();
}
And then I've setup my ViewModel like this
public class UserViewModel : ObservableObject
{
public ObservableCollection<User> Users { get; set; } = new ObservableCollection<User>();
public UserViewModel()
{
Users.Add(new User{Name = "Riley"});
Users.Add(new User{Name = "Riley1"});
}
}
Simple, now I do have a ObservableObject that looks like this and deals with the INPC
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And in my MainView.xaml
I've set the DataContext like so
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new BaseViewModel();
}
}
It's the exact same for the UserControl
And this is where I actually add the UserControl so it displays in the MainWindow
<ItemsControl ItemsSource="{Binding UserViewModel.Users}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<controls:UserCard/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Now the issue is that it doesn't bind the Data, I want to display the Name property from the Model but it's not displaying it and I am not sure why, if I try to bind it to a TextBlock property in the MainView directly it works fine.
I am unsure to why it's behaving like this and I would like to understand why.
Do I need to make use of DependencyProperties? Or is it just a case of me creating a new instance of the BaseViewModel? Where did I go wrong?
Your MainViewWindow contains an ItemsControl with the binding ItemsSource="{Binding UserViewModel.Users}", with each item being displayed with a <controls:UserCard/>. But your user control is then trying to bind to the list again with "{Binding UserViewModel.Users}". Why are you trying to display a list inside another list?
I suspect the problem here is that you think your custom UserControl's DataContext is still pointing to the BaseViewModel, like its parent. It isn't. The DataContext of each item in an ItemsControl points to it's own associated element in the list, i.e. an instance of type User.
UPDATED: Let's say you have a main view model with a list of child view models, like this:
public class MainViewModel : ViewModelBase
{
public MyChildViewModel[] MyItems { get; } =
{
new MyChildViewModel{MyCustomText = "Tom" },
new MyChildViewModel{MyCustomText = "Dick" },
new MyChildViewModel{MyCustomText = "Harry" }
};
}
public class MyChildViewModel
{
public string MyCustomText { get; set; }
}
And let's say you set your MainWindow's DataContext to an instance of MainViewModel and add a ListView:
<ListView ItemsSource="{Binding MyItems}" />
If you do this you'll see the following:
What's happening here is that the ListView is creating a container (of type ContentPresenter) for each of the three elements in the list, and setting each one's DataContext to point to its own instance of MyChildViewModel. By default ContentPresenter just calls 'ToString()' on its DataContext, so you're just seeing the name of the class it's pointing to. If you add a ToString() operator to your MyChildViewModel like this:
public override string ToString()
{
return $"MyChildViewModel: {this.MyCustomText}";
}
... then you'll see that displayed instead:
You can also override the ListViewItem's template entirely, and since it already points to its associated instance of MyChildViewModel you can just bind directly to its properties:
<ListView ItemsSource="{Binding MyItems}">
<ListView.ItemTemplate>
<DataTemplate>
<!-- One of these gets created for each element in the list -->
<Border BorderBrush="Black" BorderThickness="1" Background="CornflowerBlue" CornerRadius="5" Padding="5">
<TextBlock Text="{Binding MyCustomText}" Foreground="Yellow" />
</Border>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Which will change the display to this:
Make sense?

Set the DataContext of a View on a Navigation in XAML/WPF using MVVM

in my WPF-application i have multiple Views in a main window and i tried to implement a navigation between those.
My Problem is that i can't set the DataContext attribute of the views.
My MainWindowViewModel:
public Class MainWindowViewModel
{
public MainScreenViewModel mainScreenViewModel { get; set; }
public LevelViewModel levelViewModel { get; set; }
public ViewModelBase CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
RaisePropertyChanged(nameof(CurrentViewModel));
}
}
private AdvancedViewModelBase _currentViewModel;
}
My MainWindow:
<Window.Resources>
<DataTemplate DataType="{x:Type ViewModels:MainScreenViewModel}">
<views:MainScreen />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:LevelViewModel}">
<views:LevelView />
</DataTemplate>
</Window.Resources>
<Border>
<StackPanel>
<UserControl Content="{Binding Path=CurrentViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></UserControl>
</StackPanel>
</Border>
So the main idea is that the CurentViewModel shows on which View the navigation is at the moment (the DataTemplate shows the coreponding View to the ViewModel).
The Problem is that the shown View doesn't get the DataContext (so the properties mainScreenViewModel/levelViewModel of the MainWindowViewModel), it creates a new instance of the ViewModels.
Is it possible to hand over the properties as a DataContext to the View from the DataTemplate?
Thanks for your help!
The Content property contains
An object that contains the control's content
This means it is not the correct property to bind the view model. Instead you need to bind it to the DataContext property which contains
The object to use as data context
Now the defined templates are selected by their type like defined in the resources.
This means your code is almost correct, just change the binding of the CurrentViewModel like
<UserControl DataContext="{Binding CurrentViewModel}"/>
to get your code to work.

Binding a Listbox to a ObservableCollection

So I am trying to bind the following ViewModel:
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<ListBoxItem> _PlacesOrCities;
public ObservableCollection<ListBoxItem> PlacesOrCities
{
get { return _PlacesOrCities; }
set { _PlacesOrCities = value; RaisePropertyChanged("PlacesOrCities"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ViewModel()
{
_PlacesOrCities = new ObservableCollection<ListBoxItem>();
}
}
To the following xaml:
<ListBox Name="lbPlacesCity" ItemsSource="{Binding Path=(gms:MainWindow.ViewModel).PlacesOrCities, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate DataType="models:ListBoxItem">
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Name}" Visibility="{Binding Visibility}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the codebehind as such:
public ViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
ViewModel = new ViewModel();
DataContext = ViewModel;
}
And upon firing a button click event- I try to set the values of the observable collection using a in memory list:
private void StateProvince_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_CurrentSelectionPlaces = Canada.Provinces
.FirstOrDefault(x => x.Abbreviation == _SelectedStateProvince_ShortName)
.Place.OrderBy(x => x.Name).ToList();
foreach (var currentSelectionPlace in _CurrentSelectionPlaces)
{
ViewModel.PlacesOrCities.Add(currentSelectionPlace);
}
}
But it seems like none of the items are being added to the collection. Am I binding it incorrectly?
I've tried quite a few solutions but none of them seem to change the result- where no items in the list are being loaded into the collection properly.
EDIT:
It may be worth noting that the ListBoxItem as seen in the ViewModel is a custom model:
public class ListBoxItem
{
[J("Name")] public string Name { get; set; }
[J("PostalCodes")] public string[] PostalCodes { get; set; }
public Visibility Visibility { get; set; } = Visibility.Visible;
}
You should try to fit to the MVVM pattern, so the population of the list should occur at viewmodel level and not in the view's code behind.
You mentioned that you use a click event, instead of doing so, try to bind the command property of the button to a command in the viewmodel, see this link with an explanation of several types of commands and how to use them: https://msdn.microsoft.com/en-us/magazine/dn237302.aspx
In the other hand, if you already set the data context in the window constructor, to bind the ListBox items source you only need the name of the property to bind, "PlacesOrCities":
<ListBox Name="lbPlacesCity" ItemsSource="{Binding Path=PlacesOrCities, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate DataType="models:ListBoxItem">
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Name}" Visibility="{Binding Visibility}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
It would also be recommendable trying to load the items in the list without any template, you can use ListBox DisplayMemberPath property to display the name, and once you are able to load items, apply the style.
Also in the way you use ObservableCollection, you actually need to replace the whole collection instead of adding to fire RaisePropertyChanged, try a normal property instead.
public ObservableCollection<ListBoxItem> PlacesOrCities {get;set;} = new ObservableCollection<ListBoxItem>();
Modifying the collection will update the UI, so whenever you use Add or Clear, the UI should know it.
Hope it helps.

How to databind properties exposed from an ObservableCollection when the ObservableCollection is exposed from a view model?

I'm unable to find the correct binding syntax to data bind properties on items contained in an ObservableCollection when said ObservableCollection is exposed from a view model.
When I data bind the model's observable collection directly, it works. When I data bind the model's observable collection as exposed by the view model, it doesn't. This is a XAML/C# Windows Store 8.1 app.
What I currently get out of my skeleton test
{binding Path=ID}
{binding Path=Letter}
What I want
1
a
Here's my skeleton test
I have an Item class
Items are stored in the observable collection and I will ultimately want to bind against the ID and Letter properties:
public class Item
{
public Item(int id, char letter)
{
ID = id;
Letter = letter;
}
public int ID
{
get;
set;
}
public char Letter
{
get;
set;
}
}
The Model class contains an observable collection of items
public class Model
{
public Model()
{
}
public ObservableCollection<Item> Items
{
get
{
return _theItems;
}
set
{
_theItems = value;
}
}
ObservableCollection<Item> _theItems = new ObservableCollection<Item>();
}
I have a ViewModel that exposes the Model's observable collection
public class ViewModel
{
public ViewModel(Model model)
{
_model = model;
}
public ObservableCollection<Item> theItems
{
get
{
return _model.Items;
}
}
private Model _model;
}
My MainPage constructor creates the model with some test data, creates the ViewModel, and sets the page's DataContext to the view model:
public MainPage()
{
this.InitializeComponent();
Model model = new Model();
// some test data
model.Items.Add(new Item(1, 'a'));
_viewModel = new ViewModel(model);
this.DataContext = _viewModel;
}
My MainPage XAML looks like this
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<GridView ItemsSource="{Binding Path=theItems}" >
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>{binding Path=ID}</TextBlock>
<TextBlock>{binding Path=Letter}</TextBlock>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
This test skeleton is intended to simplify yet mimic my real goal which is to have a view model that exposes several things, one of which is an observable collection of objects whose different properties I want to bind. But binding the properties on items exposes from the observable collection eludes me.
I assume my problem relates to the XAML binding syntax. I am thinking that because I set the page's DataContext to the ViewModel, that setting the ItemsSource of the GridView to theItems would then allow me to refer to the ID and Letter properties of the items inside the collection.
When I directly bind the model's ObservableCollection to the MainPage's DataContext, all is well. But that departs from my goal with the ViewModel which is to expose additional things besides the ObservableCollection that I want to bind to from a singular view model.
I've tried various blog posts and things mentioned here on StackOverflow to no avail. Help appreciated.
Please check your XAML bindings.
...
<TextBlock Text="{Binding ID, Mode=OneTime}"/>
<TextBlock Text="{Binding Letter, Mode=OneTime}"/>
...
Consider to change the DataTemplate.
...
<DataTemplate>
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding ID,Mode=OneTime}"/>
<Run Text=" - "/>
<Run Text="{Binding Letter,Mode=OneTime}"/>
</TextBlock.Inlines>
</TextBlock>
</DataTemplate>
...

Explicit binding to an object without using the current DataContext

Description:
I have some View having its DataContext is already set to some List.
I also have in there a ComboBox that should trigger a Visibility event to a StackPanel. It is done through a property "SelectedVisibility" that is implementing a INotifyPropertyChanged.
Issue:
The property "SelectedVisibility" isn't part of the DataContext but in a ViewModel class and I cannot find any way to explicitely bind my ViewModel to reach that property.
Question:
Would you know how I could explicitely define my VM to be the DataContext of the SelectedValue binding in my ComboBox?
Code details:
View XAML:
<ComboBox ItemsSource="{Binding Source={StaticResource VisibilityEnum}}" SelectedValue="{Binding Path=SelectedVisibility}"/>
<StackPanel Visibility="{Binding Path=SelectedVisibility,Converter={StaticResource SelectedValueToVisible}}">
View Code behind:
public Counterparties_UserInputs()
{
// Cannot bind this as already bound
// this.DataContext = _VM;
InitializeComponent();
}
View Model:
public event PropertyChangedEventHandler PropertyChanged;
public string SelectedVisibility
{
get
{
return _selectedVisibility;
}
set
{
_selectedVisibility= value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedVisibility"));
}
}
}
Thank you in advance!
You can add a new dependancy property to your view, bind your view model to this property and then use this property as DataContext for your StackPanel and ComboBox. For example ("this" is the name of your view, "AdditionalContext" is the dependancy property you declare to store your viewmodel):
<StackPanel DataContext="{Binding AdditionalContext, ElementName=this}" Visibility="{Binding Path=SelectedVisibility, Converter={StaticResource SelectedValueToVisible}}"/>
However you should not do it, since it is a violation of MVVM pattern. The whole point of viewmodel is that you use it as DataContext for your view. The correct approach to your issue is to move List declaration to your viewmodel.

Categories

Resources