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?
Related
I'm having a bit of trouble to achieve the conversion of a "complex" ComboBox to an equally complex AutoCompleteBox. My goal is to be able to select and set a ShoppingCart's Item to be like one of the Items of a list. Here's the three steps to take to reproduce my situation (I'm using Stylet and its SetAndNotify() INPC method):
Create two objects, one having only a Name property and the other one having only the other object as a property
public class ItemModel : PropertyChangedBase
{
private string _name;
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
}
public class ShoppingCartModel : PropertyChangedBase
{
public ItemModel Item { get; set; }
}
initialize and Populate both the ItemsList and the Shoppingcart in the DataContext (since we're using MVVM, it's the ViewModel)
public ShoppingCartModel ShoppingCart { get; set; }
public ObservableCollection<ItemModel> ItemsList { get; set; }
public ShellViewModel()
{
ItemsList = new ObservableCollection<ItemModel>()
{
new ItemModel { Name = "T-shirt"},
new ItemModel { Name = "Jean"},
new ItemModel { Name = "Boots"},
new ItemModel { Name = "Hat"},
new ItemModel { Name = "Jacket"},
};
ShoppingCart = new ShoppingCartModel() { Item = new ItemModel() };
}
Create the AutoCompleteBox, ComboBox, and a small TextBlock inside the View to test it all out:
<Window [...] xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=DotNetProjects.Input.Toolkit">
<!-- Required Template to show the names of the Items in the ItemsList -->
<Window.Resources>
<DataTemplate x:Key="AutoCompleteBoxItemTemplate">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Background="Transparent">
<Label Content="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- AutoCompleteBox: can see the items list but selecting doesn't change ShoppingCart.Item.Name -->
<Label Content="AutoCompleteBox with ShoppingCart.Item.Name as SelectedItem:"/>
<toolkit:AutoCompleteBox ItemsSource="{Binding ItemsList}"
ValueMemberPath="Name"
SelectedItem="{Binding Path=ShoppingCart.Item.Name}"
ItemTemplate="{StaticResource AutoCompleteBoxItemTemplate}"/>
<!-- ComboBox: can see the items list and selecting changes ShoppingCart.Item.Name value -->
<Label Content="ComboBox with ShoppingCart.Item.Name as SelectedValue:"/>
<ComboBox ItemsSource="{Binding ItemsList}"
DisplayMemberPath="Name"
SelectedValue="{Binding Path=ShoppingCart.Item.Name}"
SelectedValuePath="Name"
SelectedIndex="{Binding Path=ShoppingCart.Item}" />
<!-- TextBox: Typing "Jean" or "Jacket" updates the ComboBox, but not the AutoCompleteBox -->
<Label Content="Value of ShoppingCart.Item.Name:"/>
<TextBox Text="{Binding Path=ShoppingCart.Item.Name}"/>
</StackPanel>
</window>
Changing the Binding Mode of the AutoCompleteBox's SelectedItem to TwoWay makes it print "[ProjectName].ItemModel" which means (I guess?) it's getting ItemModels and not strings, but I can't seem to make it work. Any help will be appreciated, thanks and feel free to edit my post if it's possible to make it smaller.
After a lot of attempts, I finally found the culprits :
INPC not implemented in ShoppingCartModel.Item despite the PropertyChangedBase inheritance (either implementing INPC or remove the PropertyChangedBase inheritance work)
public class ShoppingCartModel : PropertyChangedBase
{
private ItemModel _item;
public ItemModel Item
{
get => _item;
set => SetAndNotify(ref _item, value);
}
}
AutoCompleteBox's SelectedItem must be of the same type of ItemsSource, and have a TwoWay Mode Binding
<toolkit:AutoCompleteBox ItemsSource="{Binding ItemsList}"
ValueMemberPath="Name"
SelectedItem="{Binding Path=ShoppingCart.Item, Mode=TwoWay}"
ItemTemplate="{StaticResource AutoCompleteBoxItemTemplate}"/>
And finally... the most mysterious one is the ComboBox! Simply by being there it messes with the AutoCompleteBox and I have no idea why, just commenting the whole ComboBox makes it all work. If you know why the ComboBox breaks the AutoCompleteBox's binding feel free to help.
There's another problem though, when using an AutoCompleteBox inside a ListView, but it's better to create a separate question for that issue here
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.
I'm having following classes:
class MyViewModel
{
public List<MyItem> MyItems { get; set; }
public int Width { get; set; }
}
class MyItem
{
public string Name {get; set;}
}
As you see, there's a list of MyItems and Width property in the same class called MyViewModel. How can I bind a single element of that list to a Text property in XAML and Width from ViewModel to XAML's Width property? Here's my try, but I can't at the same time bind those two properties. I mean, I can bind whole list to Text property, but I don't know how could I bind a single item.
<ListView ItemsSource="{Binding MyViewModel}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="15" Width="520">
<TextBlock Width="{Binding Width}" Text="{Binding=MyItems.Name(?)}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
You should revise your design, but here is a quick fix: just introduce a readonly property, that returns the first element, so you will have this (assuming MyItems always has at least element, otherwise you will get an exception):
class MyViewModel
{
public List<MyItem> MyItems { get; set; }
public int Width { get; set; }
public MyItem FirstElement { get { return MyItems[0]; } }
}
In your xaml you bind TextBlock to this property:
<ListView ItemsSource="{Binding MyViewModel}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="15" Width="520">
<TextBlock Width="{Binding Width}" Text="{Binding=FirstElement}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
A little bit offtopic, but still important: viewmodel classes often implement INotifyPropertyChanged, so that views will be able to update themselves automatically. For the same reason List<T> should be replaced with ObservableCollection<T>.
In WPF, I have one Window containing a TabControl with four TabItems. Each TabItem has a Grid:
<TabItem Header="Input" Name="tabItem1">
<Grid></Grid>
</TabItem>
In my codebehind I need to specify a datacontext pointing to a ViewModel. Rather than having one ViewModel to handle all four tabs, I would like a ViewModel for each Tab. This would mean having different DataContexts for each time.
Is there a way to achieve this in a clean way?
You can set DataContext in XAML only by declaring instance in XAML only and bind DataContext to that instance.
But since you asked for cleaner way, so ideal would be to bind ItemsSource of TabControl to collection of ViewModels so that all tabItems automatically have different DataContext.
First create DummyViewModel and have ObservableCollection<DummyViewModel> collection in your main window ViewModel.
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
ViewModelCollection = new ObservableCollection<DummyViewModel>();
ViewModelCollection.Add(new DummyViewModel("Tab1", "Content for Tab1"));
ViewModelCollection.Add(new DummyViewModel("Tab2", "Content for Tab2"));
ViewModelCollection.Add(new DummyViewModel("Tab3", "Content for Tab3"));
ViewModelCollection.Add(new DummyViewModel("Tab4", "Content for Tab4"));
}
public ObservableCollection<DummyViewModel> ViewModelCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class DummyViewModel
{
public DummyViewModel(string name, string description)
{
Name = name;
Description = description;
}
public string Name { get; set; }
public string Description { get; set; }
}
and bind with collection in XAML like this:
<TabControl ItemsSource="{Binding ViewModelCollection}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Description}"/>
</Grid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
ItemTemplate is defined for header of tab items and ContentTemplate is defined for content of individual tabItems.
Four tab items will be created with each tab item DataContext is set to separate instance of DummyViewModel.
SnapShot:
I am working on simple card game, could you please enlight how to instantiate collection and add something to it.
Here is my basic Card class and Cards class that contains ObservableList of Cards
class Card
{
public String Name { get; set; }
}
class Cards
{
public ObservableCollection<Card> CardCollection { get; set; }
}
Here is my XAML, note that binding is OK but the list is empty I have no idea how to add items on list.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.DataContext>
<local:Cards x:Name="Cards"></local:Cards>
</Grid.DataContext>
<TextBlock Text="Cards"
FontFamily="Segoe UI"
FontSize="42"></TextBlock>
<ListView Margin="10,60,10,10" ItemsSource="{Binding CardCollection}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Height="50" Background="White">
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
You can also do like this.
class Cards
{
private ObservableCollection<Card> _CardCollection = new ObservableCollection<Card>();
public ObservableCollection<Card> CardCollection
{
get
{
return _CardCollection;
}
set
{
_CardCollection=value;
OnPropertyChanged("CardCollection"); //Implement property changed event
}
}
}
Note: You may have to implement propertychanged event if you want the ListView to reflect changes in ObservableCollection you have binded.
Edit: You can Add and Remove items from ObservableCollection from Add() and Remove() methods. Take a look at this page for all the methods available for ObservableCollection http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
You can add an item to the collection here like this
CardCollection.Add(new Card{Name="Ace of spades"});
Edit 2:
Yes you can use implement the ICommand Interface and bind a command to your controls. Take a look at this http://www.markwithall.com/programming/2013/03/01/worlds-simplest-csharp-wpf-mvvm-example.html
If you just want to get the instance binded to your XAML. You can access it like this in your Mainpage.xaml.cs
var bind = (Cards)DataContext;
You can get binded collection like this bind.CardCollection
I have found the answer.
Collection property must be instatiated before it can be used.
I have added it in constructor of my Cards class
class Cards
{
public ObservableCollection<Card> CardCollection { get; set; }
public Cards()
{
CardCollection = new ObservableCollection<Card>();
}
}