I have been trying to implement a WPF UserControl with some common functionality between a few different views without success. The UserControl is essentially a ListBox with some Previous & Next buttons and a Search filter. Previous and Next logic is easily copied and pasted, but the filtering is a pain each time, so it would be really nice to encapsulate that all into its own UserControl and ViewModel.
But I've been running into a wall to get the child UserControl/ViewModel to two way bind back to the parent VM.
This works if the child UserControl doesn't have its own ViewModel, but then I have to implement all the functionality in the code behind for that logic, which is unappealing, but not impossible.
I've boiled this down to a demo project- MRE Project - ChildVMBindingDemo
I have a MainWindow, MainWindowViewModel, MyListBoxControl, and a MyListBoxControlViewModel.
The MainWindow.xaml hosts the MyListBoxControl, and forwards two bindings to DependencyProperty in the code behind of the MyListBoxControl. That code behind then forwards those values to the MyListBoxControlViewModel. This is obviously my issue- the "traffic" hits the code behind, sets the values in the child VM, and it's a one way street from there. I've tried every combination of BindingMode, UpdateSourceTrigger, NotifyOnSourceUpdated, and NotifyOnTargetUpdated that I can think of without success.
MainWindow.xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:MyListBoxControl Grid.Column="0"
MyItems="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.MyItems}"
SelectedMyItem="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.SelectedMyItem}"
/>
</Grid>
MainWindow.xaml.cs:
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
this.DataContext = _viewModel;
}
MainWindowViewModel.cs:
public MainWindowViewModel()
{
MyItems = new ObservableCollection<MyItem>()
{
new MyItem() { Name = "One" },
new MyItem() { Name = "Two" },
new MyItem() { Name = "Thee" },
new MyItem() { Name = "Four" },
};
}
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Main View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
MyListBoxControl.xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding MyItems}"
SelectedItem="{Binding SelectedMyItem}"
SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding PrevCommand}"
>Prev</Button>
<Button Grid.Column="2"
Command="{Binding NextCommand}"
>Next</Button>
</Grid>
</Grid>
MyListBoxControl.xaml.cs:
private readonly MyListBoxControlViewModel _viewModel;
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
this.DataContext = _viewModel;
}
public static readonly DependencyProperty MyItemsProperty =
DependencyProperty.Register("MyItems", typeof(ObservableCollection<MyItem>), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, MyItemsChangedCallback));
public ObservableCollection<MyItem> MyItems
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsProperty);
set
{
SetValue(MyItemsProperty, value);
_viewModel.MyItems = MyItems;
}
}
private static void MyItemsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.MyItems = (ObservableCollection<MyItem>)e.NewValue;
}
}
public static readonly DependencyProperty SelectedMyItemProperty =
DependencyProperty.Register(nameof(SelectedMyItem), typeof(MyItem), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, SelectedMyItemChangedCallback)
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set
{
SetValue(SelectedMyItemProperty, value);
_viewModel.SelectedMyItem = SelectedMyItem;
}
}
private static void SelectedMyItemChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.SelectedMyItem = (MyItem)e.NewValue;
}
}
And finally
MyListBoxControlViewModel.cs:
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
private int _selectedIndex;
public int SelectedIndex
{
get => _selectedIndex;
set => Set(ref _selectedIndex, value);
}
private ICommand _prevCommand;
public ICommand PrevCommand => _prevCommand ?? (_prevCommand = new RelayCommand((param) => Prev(), (param) => CanPrev()));
public bool CanPrev() => SelectedIndex > 0;
private void Prev()
{
SelectedIndex--;
}
private ICommand _nextCommand;
public ICommand NextCommand => _nextCommand ?? (_nextCommand = new RelayCommand((param) => Next(), (param) => CanNext()));
public bool CanNext() => MyItems != null ? SelectedIndex < (MyItems.Count - 1) : false;
private void Next()
{
SelectedIndex++;
}
There were preexisting examples similar to this in our project (with the bindings in the code behind passing the values to the child VM)- so someone else struggled with this as well, and it looks like their solution was simply, that the child control never reported back to the parent- they were output only kinda deals.
The only thing I can really think of is to use a Messenger to send the selected value back to the parent directly, or give the child VM an Action to call and set the new value in the code behind dependency properties- but either option just screams of odorous spaghetti, and a probably an endless setter loop/stack overflow exception.
Is there a better approach here, or is there something here that I am just missing?
A control should never depend on an explicit or internal view model. It must depend on its own members, like public properties, alone. Then the data context can later bind to this public properties.
This will enable reusability independent from the actual DataContext type and eliminates redundant code (and redundant complexity) that otherwise would be necessary to delegate values to the private view model.
MVVM does not mean that each control must have its own dedicated view model. It is meant to give the application a structure. MVVM targets application level design and not control level design. A control must implement its UI related logic in its own view code. This can be in code-behind or spread across multiple classes. Such classes would be referenced directly (and not via data binding) as they share the same MVVM context. The MVVM context of UI logic is always View.
Data binding is basically a technology to decouple View and View Model (to allow the View Model to send data to the View without having to reference it - which is crucial to the MVVM pattern).
Data operations usually take place in the View Model (the owner of the data from the View perspective). View would only operate on data views (e.g. to filter or sort collections). But never on data directly.
See how the following example moved all View related logic to the control.
Your fixed and improved (in terms of design) MyListBoxControl, could look as follows:
MyListBoxControl.xaml.cs
public partial class MyListBoxControl : UserControl
{
public static RoutedCommand NextCommand { get; } = new RoutedUICommand("Select next MyItem", "NextCommand", typeof(MyListBoxControl));
public static RoutedCommand PreviousCommand { get; } = new RoutedUICommand("Select previous MyItem", "PreviousCommand", typeof(MyListBoxControl));
public ObservableCollection<MyItem> MyItemsSource
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsSourceProperty);
set => SetValue(MyItemsSourceProperty, value);
}
public static readonly DependencyProperty MyItemsSourceProperty = DependencyProperty.Register(
"MyItemsSource",
typeof(ObservableCollection<MyItem>),
typeof(MyListBoxControl),
new PropertyMetadata(default));
public int SelectedMyItemIndex
{
get => (int)GetValue(SelectedMyItemIndexProperty);
set => SetValue(SelectedMyItemIndexProperty, value);
}
public static readonly DependencyProperty SelectedMyItemIndexProperty = DependencyProperty.Register(
"SelectedMyItemIndex",
typeof(int),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set => SetValue(SelectedMyItemProperty, value);
}
public static readonly DependencyProperty SelectedMyItemProperty = DependencyProperty.Register(
"SelectedMyItem",
typeof(MyItem),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(MyItem), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyListBoxControl()
{
InitializeComponent();
this.CommandBindings.Add(new CommandBinding(NextCommand, ExecuteNextCommand, CanExecuteNextCommand));
this.CommandBindings.Add(new CommandBinding(PreviousCommand, ExecutePreviousCommand, CanExecutePreviousCommand));
}
private void CanExecutePreviousCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex > 0;
private void ExecutePreviousCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Max(this.SelectedMyItemIndex - 1, 0);
private void CanExecuteNextCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex < this.MyItemsSource.Count - 1;
private void ExecuteNextCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Min(this.SelectedMyItemIndex + 1, this.MyItemsSource.Count - 1);
}
MyListBoxControl.xaml
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MyItemsSource}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItem}"
SelectedIndex="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItemIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{x:Static local:MyListBoxControl.PreviousCommand}"
Content="Prev" />
<Button Grid.Column="2"
Command="{x:Static local:MyListBoxControl.NextCommand}"
Content="Next" />
</Grid>
</Grid>
</UserControl>
Usage example
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<MyListBoxControl MyItemsSource="{Binding MyItems}"
SelectedMyItem="{Binding SelectedMyItem}" />
</Window>
In case you meant to add behavior or change the behavior of the existing ListBox, extending ListBox would be the far better option. This would allow to template its items out of the box.
Additionally, if your primary intention was to separate view and related logic, always extend Control i.e. don't create a UserControl. It will also feel more natural to implement the control without a code-behind file. It will also enable more flexibility in terms of customization. For example, although UserControl is a ContentControl, it can't host content.
It's sure not pretty, and it doesn't smell great- but if this is your only option, here is how this could work.
I added an Action to the ViewModel, to set the DP in the code behind- note that it's only calling SetValue, and not directly setting the SelectedMyItem, which prevents the setter loop I was worried about.
MyListBoxControlViewModel.cs
public Action<MyItem> SelectedSetter { get; set; }
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
SelectedSetter?.Invoke(value);
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
and
MyListBoxControl.xmal.cs
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
_viewModel.SelectedSetter = (value) => SetValue(SelectedMyItemProperty, value);
this.DataContext = _viewModel;
}
While not great, it would probably work in limited use.
Probably smart to pass the Action in via the constructor to state its importance in the operation.
Related
Prerequisites: .NET 4.5.1
I have three TreeView controls that display three filtered variants of single collection instance. When I try to apply a filter on Items collection of one of controls this filter propagates to other controls automatically which prevents me to use different filters on different controls.
Is there any way to achieve the same result without having to maintain three instances of collections at once?
An example that shows the problem follows below. First two ListViews are bound to the same collection instance directly. Third one is bound to that instance through CompositeCollection. And the fourth is bound to independent collection. When I press "Set Filter" button ItemsControl.Items.Filter property if first ListView is set to IsAllowedItem method of WTest window. After this second istView.Items.Filter property somehow points to the same method while third and fourth ListView returns null. Another effect is that though third ListView shows null filter its collection is still filtered as you can see if you run the example. This very strange effect arises from the behavior of ItemCollection class that when based on ItemsSource property of owner element acquires underlying CollectionView from some application-wide storage via CollectionViewSource.GetDefaultCollectionView method. I don't know the reason of this implementation but suspect suspect that it's performance.
Test window WTest.xaml:
<Window x:Class="Local.WTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:local="clr-namespace:Local"
Name="_WTest" Title="WTest" Height="300" Width="600">
<Window.Resources>
<c:ArrayList x:Key="MyArray">
<s:String>Letter A</s:String>
<s:String>Letter B</s:String>
<s:String>Letter C</s:String>
</c:ArrayList>
<CompositeCollection x:Key="MyCollection" >
<CollectionContainer Collection="{StaticResource ResourceKey=MyArray}"/>
</CompositeCollection>
<c:ArrayList x:Key="AnotherArray">
<s:String>Letter A</s:String>
<s:String>Letter B</s:String>
<s:String>Letter C</s:String>
</c:ArrayList>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Name="FilterLabel1"/>
<TextBlock Grid.Row="0" Grid.Column="1" Name="FilterLabel2"/>
<TextBlock Grid.Row="0" Grid.Column="2" Name="FilterLabel3"/>
<TextBlock Grid.Row="0" Grid.Column="3" Name="FilterLabel4"/>
<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
<ListView Grid.Row="1" Grid.Column="1" Name="View2" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
<ListView Grid.Row="1" Grid.Column="2" Name="View3" ItemsSource="{StaticResource ResourceKey=MyCollection}"/>
<ListView Grid.Row="1" Grid.Column="3" Name="View4" ItemsSource="{StaticResource ResourceKey=AnotherArray}"/>
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Content="Set Filter" Click="OnSetFilterButtonClick"/>
</Grid>
</Window>
Code behind WTest.xaml.cs
namespace Local
{
using System.Windows;
public partial class WTest : Window
{
public WTest()
{
InitializeComponent();
UpdateFilterLabels();
}
private bool IsAllowedItem(object item)
{
return "Letter A" == (string)item;
}
private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
{
View1.Items.Filter = IsAllowedItem;
UpdateFilterLabels();
}
private void UpdateFilterLabels()
{
FilterLabel1.Text = (null == View1.Items.Filter) ? "No Filter" : View1.Items.Filter.Method.Name;
FilterLabel2.Text = (null == View2.Items.Filter) ? "No Filter" : View2.Items.Filter.Method.Name;
FilterLabel3.Text = (null == View3.Items.Filter) ? "No Filter" : View3.Items.Filter.Method.Name;
FilterLabel4.Text = (null == View4.Items.Filter) ? "No Filter" : View4.Items.Filter.Method.Name;
}
}
}
And result after "Set Filter" button is clicked:
Example: result of clicking "Set Filter" button
Create CollectionViewSource as a Resource.
<CollectionViewSource x:Key="CVSKey" Source="{DynamicResource MyArray}"/>
Use this CollectionViewSource as your ItemsSource . Replace your View1 as :
<!--<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{DynamicResource ResourceKey=MyArray}"/>-->
<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{Binding Source={StaticResource ResourceKey=CVSKey}}"/>
Thats it, now everything will work as you want it to.
Additionally, now you can apply filtering to this CollectionViewSource instead of View1 :
((CollectionViewSource)this.Resources["CVSKey"]).Filter += List_Filter;
void List_Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item.ToString() == "Letter A") ? true : false;
}
Create separate CollectionViewSource for separate ListBoxes to create separate views from same underlying collection.
Search google for CollectionViewSource.
Change the OnSetFilterButtonClick Method as below
private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
{
//Create a new listview by the ItemsSource,Apply Filter to the new listview
ListCollectionView listView = new ListCollectionView(View1.ItemsSource as IList);
listView.Filter = IsAllowedItem;
View1.ItemsSource = listView;
UpdateFilterLabels();
}
I found a simple solution that does not require creating a CollectionViewSource resource in XAML or a ListCollectionView in code for every collection that needs its own filter.
My solution is to use an ValueConverter that converts the ItemsSource source to a CollectionViewSource.View
ItemsSourceConverter:
[ValueConversion(typeof(IEnumerable), typeof(IEnumerable))]
public class ItemsSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is IEnumerable itemsSource && itemsSource != null)
{
return new CollectionViewSource() { Source = itemsSource }.View;
}
else
{
throw new Exception($"Value must be an {nameof(IEnumerable)}");
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
XAML:
<Window ...>
<Window.Resources>
<local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
</Window.Resources>
...
<ItemsControl Name="View1",
ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
<ItemsControl Name="View2",
ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
<ItemsControl Name="View3",
ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
</Window>
Code behind:
public partial class MainWindow : Window
{
ObservableCollection<DataClass> Collection1 { get; private set; }
ObservableCollection<DataClass> Collection2 { get; private set; }
ObservableCollection<DataClass> Collection3 { get; private set; }
public MainWindow()
{
InitializeComponent();
}
...
private void SetFilters()
{
View1.Filter = (item) =>
{
// Filter logic
};
View2.Filter = (item) =>
{
// Filter logic
};
View2.Filter = (item) =>
{
// Filter logic
};
}
...
}
MVVM ItemsControl with Filter binding
If we want to use the above solution with MVVM, we can create an attached property to bind the ItemsControl.Filter to a filter defined in the ViewModel.
Filter attached property:
public static class CollectionViewExtensions
{
public static readonly DependencyProperty FilterProperty = DependencyProperty.RegisterAttached(
"Filter",
typeof(Predicate<object>),
typeof(CollectionViewExtensions),
new PropertyMetadata(default(Predicate<object>), OnFilterChanged));
public static void SetFilter(ItemsControl element, Predicate<object> value)
{
element.SetValue(FilterProperty, value);
}
public static Predicate<object> GetFilter(ItemsControl element)
{
return (Predicate<object>)element.GetValue(FilterProperty);
}
private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ItemsControl itemsControl && itemsControl.Items.CanFilter)
{
if (e.OldValue is Predicate<object> oldPredicate)
{
itemsControl.Items.Filter -= oldPredicate;
}
if (e.NewValue is Predicate<object> newPredicate)
{
itemsControl.Items.Filter += newPredicate;
}
}
}
}
Source: https://stackoverflow.com/a/39438710/10927863
XAML:
<Window ...>
<Window.Resources>
<local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
</Window.Resources>
...
<ItemsControl ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter1}"/>
<ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter2}"/>
<ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter3}"/>
</Window>
ViewModel:
public class ViewModel
{
ObservableCollection<DataClass> Collection1 { get; private set; }
ObservableCollection<DataClass> Collection2 { get; private set; }
ObservableCollection<DataClass> Collection3 { get; private set; }
public Predicate<object> Filter1 { get; private set; }
public Predicate<object> Filter2 { get; private set; }
public Predicate<object> Filter3 { get; private set; }
...
private void SetFilters()
{
Filter1 = (item) =>
{
// Filter logic
};
Filter2 = (item) =>
{
// Filter logic
};
Filter3 = (item) =>
{
// Filter logic
};
}
...
}
I would like to get content from my combobox. I already tried some ways to do that, but It doesn't work correctly.
This is example of my combobox:
<ComboBox x:Name="cmbSomething" Grid.Column="1" Grid.Row="5" HorizontalAlignment="Center" Margin="0 100 0 0" PlaceholderText="NothingToShow">
<ComboBoxItem>First item</ComboBoxItem>
<ComboBoxItem>Second item</ComboBoxItem>
</ComboBox>
After I click the button, I want to display combobox selected item value.
string selectedcmb= cmbSomething.Items[cmbSomething.SelectedIndex].ToString();
await new Windows.UI.Popups.MessageDialog(selectedcmb, "Result").ShowAsync();
Why this code does not work?
My result instead of showing combobox content, it shows this text:
Windows.UI.Xaml.Controls.ComboBoxItem
You need the Content property of ComboBoxItem. So this should be what you want:
var comboBoxItem = cmbSomething.Items[cmbSomething.SelectedIndex] as ComboBoxItem;
if (comboBoxItem != null)
{
string selectedcmb = comboBoxItem.Content.ToString();
}
I have expanded on my suggestion regarding using models instead of direct UI code-behind access. These are the required parts:
BaseViewModel.cs
I use this in a lot of the view models in my work project. You could technically implement it directly in a view model, but I like it being centralized for re-use.
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Hashtable values = new Hashtable();
protected void SetValue(string name, object value)
{
this.values[name] = value;
OnPropertyChanged(name);
}
protected object GetValue(string name)
{
return this.values[name];
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
ComboViewModel.cs
This what you'll bind to make it easy to get values. I called it ComboViewModel because I'm only dealing with your ComboBox. You'll want a much bigger view model with a better name to handle all of your data binding.
public class ComboViewModel : BaseViewModel
{
public ComboViewModel()
{
Index = -1;
Value = string.Empty;
Items = null;
}
public int Index
{
get { return (int)GetValue("Index"); }
set { SetValue("Index", value); }
}
public string Value
{
get { return (string)GetValue("Value"); }
set { SetValue("Value", value); }
}
public List<string> Items
{
get { return (List<string>)GetValue("Items"); }
set { SetValue("Items",value); }
}
}
Window1.xaml
This is just something I made up to demonstrate/test it. Notice the various bindings.
<Window x:Class="SO37147147.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="cmbSomething" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" HorizontalAlignment="Center" MinWidth="80"
ItemsSource="{Binding Path=Items}" SelectedIndex="{Binding Path=Index}" SelectedValue="{Binding Path=Value}"></ComboBox>
<TextBox x:Name="selectedItem" MinWidth="80" Grid.Row="2" Grid.Column="0" Text="{Binding Path=Value}" />
<Button x:Name="displaySelected" MinWidth="40" Grid.Row="2" Grid.Column="1" Content="Display" Click="displaySelected_Click" />
</Grid>
</Window>
Window1.xaml.cs
Here's the code-behind. Not much to it! Everything is accessed through the dataContext instance. There's no need to know control names, etc.
public partial class Window1 : Window
{
ComboViewModel dataContext = new ComboViewModel();
public Window1()
{
InitializeComponent();
dataContext.Items=new List<string>(new string[]{"First Item","Second Item"});
this.DataContext = dataContext;
}
private void displaySelected_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(String.Format("Selected item:\n\nIndex: {0}\nValue: {1}", dataContext.Index, dataContext.Value));
}
}
You can add business logic for populating models from a database, saving changes to a database, etc. When you alter the properties of the view model, the UI will automatically be updated.
I am making a user control to represent chosen numbers (like in a lottery). The problem is that when binding to it inside a data template binding does not work.
It works correclty when hardcoding the values.
The errors are of this type and they appear for every dependency property I bind to
Error: BindingExpression path error: 'BackCheckedColor' property not found on 'NumberControlTest.Controls.NumberControl'. BindingExpression: Path='BackCheckedColor' DataItem='NumberControlTest.Controls.NumberControl'; target element is 'NumberControlTest.Controls.NumberControl' (Name='null'); target property is 'CheckedBackgroundColor' (type 'String')
What I find strange is that in this section of the error
BindingExpression: Path='BackCheckedColor' DataItem='NumberControlTest.Controls.NumberControl'
It suggests that it is trying to find the BackCheckedColor in the usercontrol itself. That does not make sense to me. Can somebody help??
User Control Xaml
<UserControl.Resources>
<local:CheckedToBrushConverter x:Key="CheckedToBrushConverter"
CheckedBackgroundColor="{Binding CheckedBackgroundColor}"
CheckedForegroundColor="{Binding CheckedForegroundColor}"
UncheckedBackgroundColor="{Binding UncheckedBackgroundColor}"
UncheckedForegroundColor="{Binding UncheckedForegroundColor}"/>
</UserControl.Resources>
<Grid Tapped="Grid_Tapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16*"/>
<ColumnDefinition Width="130*"/>
<ColumnDefinition Width="16*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="16*"/>
<RowDefinition Height="130*"/>
<RowDefinition Height="30*"/>
</Grid.RowDefinitions>
<Ellipse x:Name="Ellipse" Grid.RowSpan="3" Grid.ColumnSpan="3" Fill="{Binding IsChecked, Converter={StaticResource CheckedToBrushConverter}, ConverterParameter=background}"/>
<Viewbox Grid.Row="1" Grid.Column="1">
<TextBlock x:Name="NumberBlock" TextWrapping="Wrap" FontFamily="Segoe UI" Text="{Binding NumberValue}" Foreground="{Binding IsChecked, Converter={StaticResource CheckedToBrushConverter}, ConverterParameter=foreground}" />
</Viewbox>
</Grid>
User control code behind
public sealed partial class NumberControl : UserControl
{
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this;
}
public string UncheckedBackgroundColor
{
get { return (string)GetValue(UncheckedBackgroundColorProperty); }
set { SetValue(UncheckedBackgroundColorProperty, value); }
}
// Using a DependencyProperty as the backing store for UncheckedBackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty UncheckedBackgroundColorProperty =
DependencyProperty.Register("UncheckedBackgroundColor", typeof(string), typeof(NumberControl), new PropertyMetadata(string.Empty));
public string CheckedBackgroundColor
{
get { return (string)GetValue(CheckedBackgroundColorProperty); }
set { SetValue(CheckedBackgroundColorProperty, value); }
}
// Using a DependencyProperty as the backing store for CheckedBackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CheckedBackgroundColorProperty =
DependencyProperty.Register("CheckedBackgroundColor", typeof(string), typeof(NumberControl), new PropertyMetadata(string.Empty));
plus more dependency properties like those.
MainPage xaml
<Page.Resources>
<DataTemplate x:Key="NumberTemplate">
<Grid>
<controls:NumberControl
UncheckedBackgroundColor="{Binding BackUncheckedColor}"
UncheckedForegroundColor="{Binding ForeUncheckedColor}"
CheckedBackgroundColor="{Binding BackCheckedColor}"
CheckedForegroundColor="{Binding ForeCheckedColor}"
NumberValue="{Binding Value}"
IsChecked="{Binding IsChecked}"
HorizontalAlignment="Center"
VerticalAlignment="Center" Width="45" Height="45"/>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Background="#0f455f">
<GridView x:Name="NumbersGridView" ItemTemplate="{StaticResource NumberTemplate}" ItemsSource="{Binding Numbers, Mode=TwoWay}"/>
<Button x:Name="printButton" Content="Print" VerticalAlignment="Bottom" HorizontalAlignment="Center" Click="printButton_Click"/>
</Grid>
Model class which provides the data of the collection bound to the gridview
public class MockNumber
{
public MockNumber(bool isChecked, int value, string backchcolor, string forchcolor, string backunchcolor, string forunchcolor)
{
IsChecked = isChecked;
Value = value;
BackCheckedColor = backchcolor;
ForeCheckedColor = forchcolor;
BackUncheckedColor = backunchcolor;
ForeUncheckedColor = forunchcolor;
}
public bool IsChecked { get; set; }
public int Value { get; set; }
public string BackCheckedColor { get; set; }
public string ForeCheckedColor { get; set; }
public string BackUncheckedColor { get; set; }
public string ForeUncheckedColor { get; set; }
}
EDIT: How the model is instantiated and bound in the MainPage codebehind.
public MainPage()
{
this.InitializeComponent();
this.DataContext = this;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
makelist();
}
void makelist()
{
for (int i = 1; i <= 20; i++)
{
Numbers.Add(new MockNumber(i % 4 == 0 ? true : false, i, "#dead2b", "#000000", "#dead2b", "#f0b60c"));
}
}
private ObservableCollection<MockNumber> numbers = new ObservableCollection<MockNumber>();
public ObservableCollection<MockNumber> Numbers
{
get
{
return numbers;
}
set
{
numbers = value;
}
}
The reason why it's trying to find the 'BackCheckedColor' property from the NumberControl is because you set the user control's datacontext to itself.
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this;
}
You're telling the user control that your data context is itself. It means that when you do the "{Binding}" the path should be a property of the user control which I don't think is a good idea.
I understand that you want to bind some dependency properties to your Model class but I didn't see in your example where you instantiated the model class and use it as your data context.
Another thing to consider, you might want to use a custom control instead of a user control. I can see that you added some dependency properties to your user control but in practice, dependency properties added to custom controls and static classes that has attached properties.
EDIT:
After reading your additional code, I can see that the user control's datacontext was being set to 'this' which is itself. You need to remove that.
public sealed partial class NumberControl : UserControl
{
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this; //Remove this line
}
//...
Then after removing that, you usercontrol should inherit the GridViewItem's Binding or you can explicitly put the datacontext in your DataTemplate.
<DataTemplate x:Key="NumberTemplate">
<Grid>
<controls:NumberControl DataContext="{Binding}" <!--specify the data context-->
UncheckedBackgroundColor="{Binding BackUncheckedColor}"
//..
I am not able to figure out why my third Nested DataBinding in WPF is not working. I am using Entity Framework and Sql Server 2012 and following are my entities. An Application can have more than one accounts. There is an Accounts Table and an Applications Table.
ENTITIES
1. Applications
2. Accounts
VIEWMODELS
1. ApplicationListViewModel
2. ApplicationViewModel
3. AccountListViewModel
4. AccountViewModel
In my usercontrol I am trying to do following:
1. Use combobox to select an application using ApplicationListViewModel (Working)
2. Upon selected application display all accounts in datagrid (Working)
3. Upon selected account display details information about a particular account.(Does not show details of the selected account)
<UserControl.Resources>
<vm:ApplicationListViewModel x:Key="AppList" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource AppList}}">
<Grid>
<Grid.RowDefinitions>
...
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0">
<GroupBox Header="View all">
<StackPanel>
<!-- All Applications List -->
<ComboBox x:Name="cbxApplicationList"
ItemsSource="{Binding Path=ApplicationList}"
DisplayMemberPath="Title" SelectedValuePath="Id"
SelectedItem="{Binding Path=SelectedApplication, Mode=TwoWay}"
IsSynchronizedWithCurrentItem="True" />
<!-- Selected Application Accounts -->
<DataGrid x:Name="dtgAccounts" Height="Auto" Width="auto" AutoGenerateColumns="False"
DataContext="{Binding SelectedApplication.AccountLVM}"
ItemsSource="{Binding Path=AccountList}"
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding Path=Title}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</GroupBox>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" >
<GroupBox x:Name="grpBoxAccountDetails" Header="New Account" >
<!-- Selected Account Details -->
<!-- DataContext binding does not appear to work -->
<StackPanel DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}" >
<Grid>
<Grid.RowDefinitions>
...
</Grid.ColumnDefinitions>
<TextBlock x:Name="lblApplication" Grid.Row="0" Grid.Column="0" >Application</TextBlock>
<ComboBox x:Name="cbxApplication" Grid.Row="0" Grid.Column="1"
DataContext="{Binding Source={StaticResource AppList}}"
ItemsSource="{Binding ApplicationList}"
DisplayMemberPath="Title" SelectedValuePath="Id"
SelectedValue="{Binding SelectedApplication.AccountLVM.SelectedAccount.ApplicationId}">
</ComboBox>
<TextBlock x:Name="lblTitle" Grid.Row="0" Grid.Column="0" >Title</TextBlock>
<TextBox x:Name="txtTitle" Grid.Row="0" Grid.Column="1" Height="30" Width="200"
Text="{Binding Title}" DataContext="{Binding Mode=OneWay}"></TextBox>
<Button Grid.Row="1" Grid.Column="0" Command="{Binding AddAccount}">Add</Button>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</Grid>
</StackPanel>
ApplicationListViewModel
class ApplicationListViewModel : ViewModelBase
{
myEntities context = new myEntities();
private static ApplicationListViewModel instance = null;
private ObservableCollection<ApplicationViewModel> _ApplicationList = null;
public ObservableCollection<ApplicationViewModel> ApplicationList
{
get
{
return GetApplications();
}
set {
_ApplicationList = value;
OnPropertyChanged("ApplicationList");
}
}
//public ObservableCollection<ApplicationViewModel> Cu
private ApplicationViewModel selectedApplication = null;
public ApplicationViewModel SelectedApplication
{
get
{
return selectedApplication;
}
set
{
selectedApplication = value;
OnPropertyChanged("SelectedApplication");
}
}
//private ICommand showAddCommand;
public ApplicationListViewModel()
{
this._ApplicationList = GetApplications();
}
internal ObservableCollection<ApplicationViewModel> GetApplications()
{
if (_ApplicationList == null)
_ApplicationList = new ObservableCollection<ApplicationViewModel>();
_ApplicationList.Clear();
foreach (Application item in context.Applications)
{
ApplicationViewModel a = new ApplicationViewModel(item);
_ApplicationList.Add(a);
}
return _ApplicationList;
}
public static ApplicationListViewModel Instance()
{
if (instance == null)
instance = new ApplicationListViewModel();
return instance;
}
}
ApplicationViewModel
class ApplicationViewModel : ViewModelBase
{
private myEntities context = new myEntities();
private ApplicationViewModel originalValue;
public ApplicationViewModel()
{
}
public ApplicationViewModel(Application acc)
{
//Initialize property values
this.originalValue = (ApplicationViewModel)this.MemberwiseClone();
}
public ApplicationListViewModel Container
{
get { return ApplicationListViewModel.Instance(); }
}
private AccountListViewModel _AccountLVM = null;
public AccountListViewModel AccountLVM
{
get
{
return GetAccounts();
}
set
{
_AccountLVM = value;
OnPropertyChanged("AccountLVM");
}
}
internal AccountListViewModel GetAccounts()
{
_AccountLVM = new AccountListViewModel();
_AccountLVM.AccountList.Clear();
foreach (Account i in context.Accounts.Where(x=> x.ApplicationId == this.Id))
{
AccountViewModel account = new AccountViewModel(i);
account.Application = this;
_AccountLVM.AccountList.Add(account);
}
return _AccountLVM;
}
}
AccountListViewModel
class AccountListViewModel : ViewModelBase
{
myEntities context = new myEntities();
private static AccountListViewModel instance = null;
private ObservableCollection<AccountViewModel> _accountList = null;
public ObservableCollection<AccountViewModel> AccountList
{
get
{
if (_accountList != null)
return _accountList;
else
return GetAccounts();
}
set {
_accountList = value;
OnPropertyChanged("AccountList");
}
}
private AccountViewModel selectedAccount = null;
public AccountViewModel SelectedAccount
{
get
{
return selectedAccount;
}
set
{
selectedAccount = value;
OnPropertyChanged("SelectedAccount");
}
}
public AccountListViewModel()
{
this._accountList = GetAccounts();
}
internal ObservableCollection<AccountViewModel> GetAccounts()
{
if (_accountList == null)
_accountList = new ObservableCollection<AccountViewModel>();
_accountList.Clear();
foreach (Account item in context.Accounts)
{
AccountViewModel a = new AccountViewModel(item);
_accountList.Add(a);
}
return _accountList;
}
public static AccountListViewModel Instance()
{
if (instance == null)
instance = new AccountListViewModel();
return instance;
}
}
AccountViewModel. I am eliminating all other initialization logic aside in viewmodel for simplicity.
class AccountViewModel : ViewModelBase
{
private myEntites context = new myEntities();
private AccountViewModel originalValue;
public AccountViewModel()
{
}
public AccountViewModel(Account acc)
{
//Assign property values.
this.originalValue = (AccountViewModel)this.MemberwiseClone();
}
public AccountListViewModel Container
{
get { return AccountListViewModel.Instance(); }
}
public ApplicationViewModel Application
{
get;
set;
}
}
Edit1:
When I data bind to view the details of the SelectedAccount with textbox it doesn't show any text.
1. Able to databind to ApplicationListViewModel to Combobox.
2. Successfully Bind to view AccountList based upon SelectedApplication
3. Unable to Bind to SelectedAcount in the AccountListViewModel.
I think in the following line it doesn't show any details about the selected account. I have checked all databinding syntax. In the properties I am able to view appropriate DataContext and bind to the properties. But it doesn't show any text. When I select each individual record in the DataGrid I am able to debug the call and select the object but somehow that object is not being shown in the textbox at the very end.
DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}"
Edit2:
Based upon the suggestion in the comment below I tried snoop and was able to see the title textbox row highlighted in red color. I am trying to change the binding Path property and datacontext but still not working. When I tried to click on the "Delve Binding Expression" it gave me unhandled exception. I don't know what that means if as it came from Snoop.
Edit3:
I have taken screenshots of DataContext Property for the StackPanel for the Account Details section and the text property of the textbox.
Solution:
Based upon suggestions below I have made following changes to my solution and made it way more simple. I made it unnecessarily complex.
1. AccountsViewModel
2. AccountViewModel
3. ApplicationViewModel
Now I have created properties as SelectedApplication, SelectedAccount all in just one AccountsViewModel. Removed all complex DataContext syntax and now there is just one DataContext in the xaml page.
Simplified code.
class AccountsViewModel: ViewModelBase
{
myEntities context = new myEntities();
private ObservableCollection<ApplicationViewModel> _ApplicationList = null;
public ObservableCollection<ApplicationViewModel> ApplicationList
{
get
{
if (_ApplicationList == null)
{
GetApplications();
}
return _ApplicationList;
}
set
{
_ApplicationList = value;
OnPropertyChanged("ApplicationList");
}
}
internal ObservableCollection<ApplicationViewModel> GetApplications()
{
if (_ApplicationList == null)
_ApplicationList = new ObservableCollection<ApplicationViewModel>();
else
_ApplicationList.Clear();
foreach (Application item in context.Applications)
{
ApplicationViewModel a = new ApplicationViewModel(item);
_ApplicationList.Add(a);
}
return _ApplicationList;
}
//Selected Application Property
private ApplicationViewModel selectedApplication = null;
public ApplicationViewModel SelectedApplication
{
get
{
return selectedApplication;
}
set
{
selectedApplication = value;
this.GetAccounts();
OnPropertyChanged("SelectedApplication");
}
}
private ObservableCollection<AccountViewModel> _accountList = null;
public ObservableCollection<AccountViewModel> AccountList
{
get
{
if (_accountList == null)
GetAccounts();
return _accountList;
}
set
{
_accountList = value;
OnPropertyChanged("AccountList");
}
}
//public ObservableCollection<AccountViewModel> Cu
private AccountViewModel selectedAccount = null;
public AccountViewModel SelectedAccount
{
get
{
return selectedAccount;
}
set
{
selectedAccount = value;
OnPropertyChanged("SelectedAccount");
}
}
internal ObservableCollection<AccountViewModel> GetAccounts()
{
if (_accountList == null)
_accountList = new ObservableCollection<AccountViewModel>();
else
_accountList.Clear();
foreach (Account item in context.Accounts.Where(x => x.ApplicationId == this.SelectedApplication.Id))
{
AccountViewModel a = new AccountViewModel(item);
_accountList.Add(a);
}
return _accountList;
}
}
XAML Side
<UserControl.Resources>
<vm:AccountsViewModel x:Key="ALVModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource ALVModel}}" Margin="0,0,-390,-29">
<StackPanel>
<ComboBox x:Name="cbxApplicationList"
ItemsSource="{Binding Path=ApplicationList}"
DisplayMemberPath="Title" SelectedValuePath="Id"
SelectedItem="{Binding Path=SelectedApplication, Mode=TwoWay}"
IsSynchronizedWithCurrentItem="True"></ComboBox>
<DataGrid x:Name="dtgAccounts" Height="Auto" Width="auto"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=AccountList}"
SelectedItem="{Binding SelectedAccount, Mode=TwoWay}"
IsSynchronizedWithCurrentItem="True" >
<DataGrid.Columns>
<DataGridTextColumn Header="Title" Binding="{Binding Path=Title}"></DataGridTextColumn>
<DataGridTextColumn Header="CreatedDate" Binding="{Binding Path=CreatedDate}"></DataGridTextColumn>
<DataGridTextColumn Header="LastModified" Binding="{Binding Path=LastModifiedDate}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<StackPanel Height="Auto" Width="300" HorizontalAlignment="Left" DataContext="{Binding Path=SelectedAccount}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"></ColumnDefinition>
<ColumnDefinition Width="200"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock x:Name="lblTitle" Grid.Row="0" Grid.Column="0" >Title</TextBlock>
<TextBox x:Name="txtTitle" Grid.Row="0" Grid.Column="1" Height="30" Width="200"
Text="{Binding Title}"></TextBox>
</Grid>
</StackPanel>
</StackPanel>
I didn't understood MVVM concept properly. I tried to build everything modular and in the end I screwed it up.
I suspect your problem is related to the fact you are returning a new ObservableCollection every time you call the setter for AccountLVM, and you are not raising your PropertyChange notification, so any existing bindings do not get updated
public AccountListViewModel AccountLVM
{
get
{
return GetAccounts();
}
set
{
_AccountLVM = value;
OnPropertyChanged("AccountLVM");
}
}
internal AccountListViewModel GetAccounts()
{
_AccountLVM = new AccountListViewModel();
_AccountLVM.AccountList.Clear();
foreach (Account i in context.Accounts.Where(x=> x.ApplicationId == this.Id))
{
AccountViewModel account = new AccountViewModel(i);
account.Application = this;
_AccountLVM.AccountList.Add(account);
}
return _AccountLVM;
}
I find your bindings very confusing and hard to follow, however I think whenever this gets evaluated
DataContext="{Binding SelectedApplication.AccountLVM.SelectedAccount}"
it is creating a new AccountLVM, which does not have the SelectedAccount property set.
You don't see the existing DataGrid.SelectedItem change at all because it's still bound to the old AccountLVM as no PropertyChange notification got raised when _accountLVM changed, so the binding doesn't know to update.
But some other miscellaneous related to your code:
Don't change the private version of the property unless you also raise the PropertyChange notification for the public version of the property. This applies to both your constructors and your GetXxxxx() methods like GetAccounts().
Don't return a method call from your getter. Instead set the value using your method call if it's null, and return the private property afterwards.
public AccountListViewModel AccountLVM
{
get
{
if (_accountLVM == null)
GetAccounts(); // or _accountLVM = GetAccountLVM();
return _accountLVM;
}
set { ... }
}
It's really confusing to have the DataContext set in so many controls. The DataContext is the data layer behind your UI, and it's easiest if your UI simply reflects the data layer, and having to go all over the place to get your data makes the data layer really hard to follow.
If you must make a binding to something other than the current data context, try to use other binding properties to specify a different binding Source before immediately going to change the DataContext. Here's an example using the ElementName property to set the binding source:
<TextBox x:Name="txtTitle" ...
Text="{Binding ElementName=dtgAccounts, Path=SelectedItem.Title}" />
The DataContext in inherited, so you don't ever need to write DataContext="{Binding }"
You may want to consider re-writing your parent ViewModel so you can setup XAML like this, without all the extra DataContext bindings or 3-part nested properties.
<ComboBox ItemsSource="{Binding ApplicationList}"
SelectedItem="{Binding SelectedApplication}" />
<DataGrid ItemsSource="{Binding SelectedApplication.Accounts}"
SelectedItem="{Binding SelectedAccount}" />
<StackPanel DataContext="{Binding SelectedAccount}">
...
</StackPanel>
If you're new to the DataContext or struggling to understand it, I'd recommend reading this article on my blog to get a better understanding of what it is and how it works.
Well one major problem with this Binding method is, that the value is only updated, when the last property value, in your case SelectedAccount, is changed. The other levels are not watched by the BindingExpression, so if e.g. SelectedApplication.AccountLVM is changed the DataContext will not notice a difference in SelectedAccount because the binding is still 'watching' on the old reference and you're modifying another reference in your VM.
So I think at the start of the application SelectedApplication is null and the Binding of the ComboBox doesn't notice that it changes. Hmm, I thought about another binding solution, but I couldn't found one. So I suggest, that you create an additional property for reflecting SelectedAccount in your ApplicationListViewModel class.
I need to create an UI which allows me to select entries from one list box and add it to another listbox at the run time. Now, the listbox1 may contain combo box and checkbox as the items.
For example, if I add a combo box labelled Quarter with values "Q1, Q2, Q3, Q4" as an item in listbox1 and select the entry Q1 in it, and click on the "Add" button, it should be added to listbox2. Vice versa should also be possible. This should be possible at the run time. How could I add combo box and checkbox as an item to the listbox? Also, please suggest if for the add-remove buttons, the code I've is correct.
private void MoveListBoxItems(ListBox source, ListBox destination)
{
ListBox.SelectedObjectCollection sourceItems = source.SelectedItems;
foreach (var item in sourceItems)
{
destination.Items.Add(item);
}
while (source.SelectedItems.Count > 0)
{
source.Items.Remove(source.SelectedItems[0]);
}
}
private void button1_Click(object sender, EventArgs e)
{
MoveListBoxItems(listBox1, listBox2);
}
private void button2_Click(object sender, EventArgs e)
{
MoveListBoxItems(listBox2, listBox1);
}
This is a WPF solution to your need. I am posting it because you told me it could be useful for you. It largely surpasses anything you can ever hope to achieve in winforms, which is a very limited and outdated technology.
This is how it looks in my screen:
I am using some simple ViewModels to represent the data:
ListItemViewModel (the "base" one):
public class ListItemViewModel: ViewModelBase
{
private string _displayName;
public string DisplayName
{
get { return _displayName; }
set
{
_displayName = value;
NotifyPropertyChange(() => DisplayName);
}
}
}
BoolListItemViewModel (for CheckBoxes):
public class BoolListItemViewModel: ListItemViewModel
{
private bool _value;
public bool Value
{
get { return _value; }
set
{
_value = value;
NotifyPropertyChanged(() => Value);
}
}
}
SelectableListItemViewModel (for ComboBoxes):
public class SelectableListItemViewModel: ListItemViewModel
{
private ObservableCollection<ListItemViewModel> _itemsSource;
public ObservableCollection<ListItemViewModel> ItemsSource
{
get { return _itemsSource ?? (_itemsSource = new ObservableCollection<ListItemViewModel>()); }
}
private ListItemViewModel _selectedItem;
public ListItemViewModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
NotifyPropertyChange(() => SelectedItem);
}
}
}
This is the "Main" ViewModel, which holds the 2 lists and the Commands (the Button actions)
public class ListBoxSampleViewModel: ViewModelBase
{
private ObservableCollection<ListItemViewModel> _leftItems;
public ObservableCollection<ListItemViewModel> LeftItems
{
get { return _leftItems ?? (_leftItems = new ObservableCollection<ListItemViewModel>()); }
}
private ObservableCollection<ListItemViewModel> _rightItems;
public ObservableCollection<ListItemViewModel> RightItems
{
get { return _rightItems ?? (_rightItems = new ObservableCollection<ListItemViewModel>()); }
}
private DelegateCommand<ListItemViewModel> _moveToRightCommand;
public DelegateCommand<ListItemViewModel> MoveToRightCommand
{
get { return _moveToRightCommand ?? (_moveToRightCommand = new DelegateCommand<ListItemViewModel>(MoveToRight)); }
}
private void MoveToRight(ListItemViewModel item)
{
if (item != null)
{
LeftItems.Remove(item);
RightItems.Add(item);
}
}
private DelegateCommand<ListItemViewModel> _moveToLeftCommand;
public DelegateCommand<ListItemViewModel> MoveToLeftCommand
{
get { return _moveToLeftCommand ?? (_moveToLeftCommand = new DelegateCommand<ListItemViewModel>(MoveToLeft)); }
}
private void MoveToLeft(ListItemViewModel item)
{
if (item != null)
{
RightItems.Remove(item);
LeftItems.Add(item);
}
}
}
This is the entire XAML for the Window:
<Window x:Class="WpfApplication4.Window14"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication4"
Title="Window14" Height="300" Width="300">
<Window.Resources>
<DataTemplate DataType="{x:Type local:ListItemViewModel}">
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:BoolListItemViewModel}">
<CheckBox Content="{Binding DisplayName}" IsChecked="{Binding Value}" HorizontalAlignment="Left"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SelectableListItemViewModel}">
<ComboBox ItemsSource="{Binding ItemsSource}" SelectedItem="{Binding SelectedItem}"
HorizontalAlignment="Stretch" MinWidth="100"/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding LeftItems}"
x:Name="LeftList"/>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<Button Content="Move to Right"
Command="{Binding MoveToRightCommand}"
CommandParameter="{Binding SelectedItem,ElementName=LeftList}"/>
<Button Content="Move to Left"
Command="{Binding MoveToLeftCommand}"
CommandParameter="{Binding SelectedItem,ElementName=RightList}"/>
</StackPanel>
<ListBox ItemsSource="{Binding RightItems}"
Grid.Column="2" x:Name="RightList"/>
</Grid>
</Window>
and finally, this is the Window Code-behind, which only initializes the ViewModel with some items:
public partial class Window14 : Window
{
public Window14()
{
InitializeComponent();
DataContext = new ListBoxSampleViewModel()
{
LeftItems =
{
new ListItemViewModel(){DisplayName = "Item1"},
new BoolListItemViewModel() {DisplayName = "Check Item 2", Value = true},
new SelectableListItemViewModel()
{
ItemsSource =
{
new ListItemViewModel() {DisplayName = "Combo Item 1"},
new BoolListItemViewModel() {DisplayName = "Check inside Combo"},
new SelectableListItemViewModel()
{
ItemsSource =
{
new ListItemViewModel() {DisplayName = "Wow, this is awesome"},
new BoolListItemViewModel() {DisplayName = "Another CheckBox"}
}
}
}
}
}
};
}
}
At first glance, this might seem like a LOT of code... but if you take 2 seconds to analyze it... Its just "simple, simple properties and INotifyPropertyChanged. That's how you program in WPF.
I'm talking about a completely different paradigm from what you might be used to in winforms, but it's really worth the effort of learning it. Notice that nowhere in my code I am interacting with UI elements. I just create the ViewModel structure and let the WPF Binding System to take care of generating the UI for me, using the provided DataTemplates.
I'm using the ViewModelBase from MVVM Light and the DelegateCommand from WPFTutorial.net. You can copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself (you will also need these 2 classes from the links above)
If you need to integrate this in an existing winforms application, you will need the ElementHost