Extending WPF ListBoxItem for sourced items not working - c#

If I create a class that extends WPF's ListBoxItem, create a list of these objects, try to bind the list to a ListBox's ItemsSource, the items will not display:
<ListBox ItemsSource="{Binding Path=LbData, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}">
<ListBox.ItemTemplate>
<DataTemplate >
<TextBlock Text="{Binding Path=Display}" Margin="1" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
public partial class MainWindow : Window
{
public IEnumerable lbData = new List<LbItem>();
public IEnumerable LbData
{
get { return lbData; }
set { lbData = value; }
}
public MainWindow()
{
InitializeComponent();
LbData = new List<LbItem> { new LbItem("a"), new LbItem("b") };
}
}
public class LbItem : ListBoxItem
{
public string Display { get; private set; }
public LbItem(string v)
{
Display = v;
}
}
I'm new to WPF and don't see why this should be an issue. TIA

The ItemsSource normally is used for data-objects, and what you do there (setting the ItemTemplate) suggest that you should not make your object inherit from ListBoxItem at all, instead it should be a normal object (possibly implementing INotifyPropertyChanged if properties may change after creation). If the list changes it should implement INotifyCollectionChanged.
Because the items are already ListBoxItems the DataTemplate you set will be disregarded. There should be the following error in your Visual Studio Output-window:
System.Windows.Data Error: 26 : ItemTemplate and ItemTemplateSelector are ignored for items already of the ItemsControl's container type; Type='LbItem'

try setting LbData before InitializeComponent(); or Implement INotifyPropertyChanged also you probably would want to bind to an ObservableCollection

Related

Selecting the last TabItem when new items are added to a TabControl's ItemSource using MVVM

I have created a dynamically generated TabControl by binding ItemsSource to MyUnicornsViewModel.
As new items are added to MyUnicornsViewModel... new tab items are created. However, the newly added tabs are not automatically selected in the TabControl.
How can I get new tabs to be selected when they are added?
<TabControl ItemsSource="{Binding MyUnicornsViewModel}" SelectedItem="{Binding SelectedItem}">
<TabControl.ItemTemplate>
<!-- header template -->
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- body template-->
<DataTemplate>
<TextBlock Text="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
At first, I was hoping there was an event for "ItemsChanged" or "ItemAdded" in the TabControl, that way I can set the SelectedIndex in the code-behind as new items are added.
Another thing I tried was to bind the TabControl.SelectedItem to a SelectedItem property in MyUnicornsViewModel. Sadly, that didn't work either.
MyUnicornsViewModel:
public class MyUnicornsViewModel : ObservableCollection<UnicornViewModel>
{
...
private void AddNewUnicorn()
{
var awesomeUnicorn = new UnicornViewModel();
Add(awesomeUnicorn);
SelectedItem = awesomeUnicorn; //I expected my TabControl to have 'awesomeUnicorn' selected.
}
public UnicornViewModel SelectedItem { get; set; }
}
There are a couple of issues here:
It's very odd to derive a "view model" from ObservableCollection. A view model should contain an observable collection.
View models need to implement the INotifyPropertyChanged interface; it's not clear from the code provide if UnicornViewModel implements this interface, however, MyUnicornsViewModel absolutely does not.
Here's some suggestions:
A view model base class that implements the INotifyPropertyChanged interface will really help get you most of the way. You can write your own using the INotifyPropertyChanged documentation or look for an MVVM framework that fits well with your project (e.g. Prism, MVVM Light, ReactiveUI). Each of these will provide a base class to use for view models - BindableBase, ViewModelBase, ReactiveObject respectively for each of the frameworks above.
MyUnicornsViewModel should have:
An ObservableCollection for the collection of unicorns; this will be bound to the ItemsSource property on your TabControl.
The SelectedItem property must fire the PropertyChanged event when set.
Here's a quick sample using Prism:
public sealed class UnicornViewModel : BindableBase
{
public UnicornViewModel(string name, string content)
{
Name = name;
Content = content;
}
// these properties don't change and therefore don't need to raise property changed
public string Name { get; }
public string Content { get; }
}
public sealed class UnicornsViewModel : BindableBase
{
private UnicornViewModel _selectedUnicorn;
public UnicornsViewModel()
{
AddUnicornCommand = new DelegateCommand(AddUnicorn);
ClearUnicornsCommand = new DelegateCommand(ClearUnicorns, () => HasUnicorns).ObservesProperty(() => HasUnicorns);
}
public ObservableCollection<UnicornViewModel> Unicorns { get; } = new ObservableCollection<UnicornViewModel>();
public UnicornViewModel SelectedUnicorn
{
get => _selectedUnicorn;
set => SetProperty(ref _selectedUnicorn, value, () => RaisePropertyChanged(nameof(HasUnicorns)));
}
public DelegateCommand AddUnicornCommand { get; }
public DelegateCommand ClearUnicornsCommand { get; }
private bool HasUnicorns => Unicorns.Any(); // helper property for the clear command's can execute
private void AddUnicorn()
{
Unicorns.Add(new UnicornViewModel($"Unicorn {Unicorns.Count + 1}", Guid.NewGuid().ToString()));
SelectedUnicorn = Unicorns.Last();
}
private void ClearUnicorns()
{
SelectedUnicorn = null;
Unicorns.Clear();
}
}

Converting a ComboBox to an AutoCompleteBox using the WPF Toolkit

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

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?

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.

Getting Items From Within ItemsControl (Understanding DataContext)

I am populating an ItemsControl with various elements, including Buttons and ComboBox elements. Accessing and populating the elements is simple, but I'm stuck on how to detect and associate which Item in the ItemsControl the ComboBox (or Button) belongs to.
To help illustrate the problem, consider the following basic UI:
Now, when I use the ComboBox or Button I want to be able to associate that use only with the ItemControl Item it's a part of. However, currently, if I select an item in the ComboBox every ComboBox in the ItemsControl will reflect that change.
I can capture the SelectedItem in the below ListBox, but ideally, I would like to be able to display both the SelectedItem and which ItemControl Item it came from. For instance, ComboBoxItem1, My First Property - From Item (1).
I am strictly adhering to MVVM principals, and consequently, I am not looking for any solutions using code-behind.
TL;DR
I know the code can become unwieldy. I believe the above description is adequate to state my problem, but I am including the basic boiler plate code below in case it's helpful in posting an answer. (Obviously, I have implemented INotifyProperty and ICommand elsewhere):
MainWindowView.xaml
<ItemsControl Width="300" Height="200" ItemsSource="{Binding MyObservableCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="2" Margin="10">
<StackPanel Margin="0,10,0,10">
<TextBlock Margin="10,0,0,0" Text="{Binding MyProperty}" FontWeight="Bold"/>
<ComboBox Width="270" Text="myBox" ItemsSource="{Binding DataContext.ComboOptions, RelativeSource={RelativeSource AncestorType=ItemsControl}}" DisplayMemberPath="ListItem" SelectedItem="{Binding DataContext.SelectedItem, RelativeSource={RelativeSource AncestorType=Window}}"/>
<RadioButton Width ="270" Content="Button1" Command="{Binding DataContext.GetButtonCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="Button1" Style="{DynamicResource {x:Type ToggleButton}}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
MyComboBoxOptionsViewModel.cs
public class MyComboBoxOptionsViewModel : ObservableObject
{
private MyComboBoxOptionsModel _myComboBoxOptions = new MyComboBoxOptionsModel();
public MyComboBoxOptionsViewModel(MyComboBoxOptionsModel _myComboBoxOptions)
{
this._myComboBoxOptions = _myComboBoxOptions;
}
public string ComboBoxOption
{
get { return _myComboBoxOptions.ComboBoxOption; }
set
{
_myComboBoxOptions.ComboBoxOption = value;
RaisePropertyChangedEvent("ComboBoxOption");
}
}
}
MyComboBoxOptionsModel.cs
public class MyComboBoxOptionsModel
{
public string ComboBoxOption { get; set; }
}
MainWindowViewModel.cs
public class MainWindowViewModel : ObservableObject
{
private ObservableCollection<string> _messages = new ObservableCollection<string>();
private ObservableCollection<MyViewModel> _myObservableCollection = new ObservableCollection<MyViewModel>();
private List<MyComboBoxOptionsViewModel> _comboOptions = new List<MyComboBoxOptionsViewModel>();
private MyComboBoxOptionsViewModel _selectedItem = new MyComboBoxOptionsViewModel(null);
public MainWindowViewModel()
{
_myObservableCollection.Add(new MyViewModel(new MyModel { MyProperty = "My First Property" }));
_myObservableCollection.Add(new MyViewModel(new MyModel { MyProperty = "My Second Property" }));
_comboOptions.Add(new MyComboBoxOptionsViewModel(new MyComboBoxOptionsModel { ComboBoxOption = "Option1" }));
_comboOptions.Add(new MyComboBoxOptionsViewModel(new MyComboBoxOptionsModel { ComboBoxOption = "Option2" }));
_comboOptions.Add(new MyComboBoxOptionsViewModel(new MyComboBoxOptionsModel { ComboBoxOption = "Option3" }));
}
public MyComboBoxOptionsViewModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
_messages.Add(_selectedItem.ComboBoxOption);
RaisePropertyChangedEvent("SelectedItem");
}
}
public List<MyComboBoxOptionsViewModel> ComboOptions
{
get { return _comboOptions; }
set
{
if (value != _comboOptions)
{
_comboOptions = value;
RaisePropertyChangedEvent("ComboOptions");
}
}
}
public ObservableCollection<MyViewModel> MyObservableCollection
{
get { return _myObservableCollection; }
set
{
if (value != _myObservableCollection)
{
_myObservableCollection = value;
RaisePropertyChangedEvent("MyObservableCollection");
}
}
}
public ObservableCollection<string> Messages
{
get { return _messages; }
set
{
if (value != _messages)
{
_messages = value;
RaisePropertyChangedEvent("Messages");
}
}
}
}
I'm looking at the UI you want and think you basically need a main view model with a collection of item view models.
In that item view model create a command and a selected item property you can bind in your template to the combo box and button. That gives you a strict mvvm binding to a single instance of the combo box value and a command which is executed by the single instance of the button.
Your bindings for combo box items will then need an explicit source as part of the binding so you can hook into one collection of values from the main view model. Or add a collection to your item view model and keep it all nice a clean and together.
As you mention, you're code is very detailed - which is great - but I may have missed some other meaning from it.
Apologies if this is an answer to the wrong question :)

Categories

Resources