I have a WPF window that uses multiple viewmodel objects as its DataContext. The window has a control that binds to a property that exists only in some of the viewmodel objects. How can I bind to the property if it exists (and only if it exists).
I am aware of the following question/answer: MVVM - hiding a control when bound property is not present. This works, but gives me a warning. Can it be done without the warning?
Thanks!
Some example code:
Xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="1" Name="ListView" Margin="25,0,25,0" ItemsSource="{Binding Path=Lst}"
HorizontalContentAlignment="Center" SelectionChanged="Lst_SelectionChanged">
</ListBox>
<local:SubControl Grid.Row="3" x:Name="subControl" DataContext="{Binding Path=SelectedVM}"/>
</Grid>
SubControl Xaml:
<UserControl x:Class="WpfApplication1.SubControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
<CheckBox IsChecked="{Binding Path=Always}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
<StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
<CheckBox IsChecked="{Binding Path=Sometimes}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
</Grid>
MainWindow Code Behind:
public partial class MainWindow : Window
{
ViewModel1 vm1;
ViewModel2 vm2;
MainViewModel mvm;
public MainWindow()
{
InitializeComponent();
vm1 = new ViewModel1();
vm2 = new ViewModel2();
mvm = new MainViewModel();
mvm.SelectedVM = vm1;
DataContext = mvm;
}
private void Lst_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox lstBx = sender as ListBox;
if (lstBx != null)
{
if (lstBx.SelectedItem.Equals("VM 1"))
mvm.SelectedVM = vm1;
else if (lstBx.SelectedItem.Equals("VM 2"))
mvm.SelectedVM = vm2;
}
}
}
MainViewModel (DataContext of MainWindow):
public class MainViewModel : INotifyPropertyChanged
{
ObservableCollection<string> lst;
ViewModelBase selectedVM;
public event PropertyChangedEventHandler PropertyChanged;
public MainViewModel()
{
Lst = new ObservableCollection<string>();
Lst.Add("VM 1");
Lst.Add("VM 2");
}
public ObservableCollection<string> Lst
{
get { return lst; }
set
{
lst = value;
OnPropertyChanged("Lst");
}
}
public ViewModelBase SelectedVM
{
get { return selectedVM; }
set
{
if (selectedVM != value)
{
selectedVM = value;
OnPropertyChanged("SelectedVM");
}
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
ViewModel1 (with sometimes property):
public class ViewModel1 : ViewModelBase, INotifyPropertyChanged
{
private bool _always;
private string _onOffAlways;
private bool _sometimes;
private string _onOffSometimes;
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel1()
{
_always = false;
_onOffAlways = "Always Off";
_sometimes = false;
_onOffSometimes = "Sometimes Off";
}
public bool Always
{
get { return _always; }
set
{
_always = value;
if (_always)
OnOffAlways = "Always On";
else
OnOffAlways = "Always Off";
OnPropertyChanged("Always");
}
}
public string OnOffAlways
{
get { return _onOffAlways; }
set
{
_onOffAlways = value;
OnPropertyChanged("OnOffAlways");
}
}
public bool Sometimes
{
get { return _sometimes; }
set
{
_sometimes = value;
if (_sometimes)
OnOffSometimes = "Sometimes On";
else
OnOffSometimes = "Sometimes Off";
OnPropertyChanged("Sometimes");
}
}
public string OnOffSometimes
{
get { return _onOffSometimes; }
set
{
_onOffSometimes = value;
OnPropertyChanged("OnOffSometimes");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
ViewModel2 (without Sometimes property):
public class ViewModel2 : ViewModelBase, INotifyPropertyChanged
{
private bool _always;
private string _onOffAlways;
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel2()
{
_always = false;
_onOffAlways = "Always Off";
}
public bool Always
{
get { return _always; }
set
{
_always = value;
if (_always)
OnOffAlways = "Always On";
else
OnOffAlways = "Always Off";
OnPropertyChanged("Always");
}
}
public string OnOffAlways
{
get { return _onOffAlways; }
set
{
_onOffAlways = value;
OnPropertyChanged("OnOffAlways");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public class AlwaysVisibleConverter : IValueConverter
{
#region Implementation of IValueConverter
public object Convert(object value,
Type targetType, object parameter, CultureInfo culture)
{
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
There are many different ways one could approach your scenario. For what it's worth, the solution you already have seems reasonable to me. The warning you get (I presume you are talking about the error message output to the debug console) is reasonably harmless. It does imply a potential performance issue, as it indicates WPF is recovering from an unexpected condition. But I would expect the cost to be incurred only when the view model changes, which should not be frequent enough to matter.
Another option, which is IMHO the preferred one, is to just use the usual WPF data templating features. That is, define a different template for each view model you expect, and then let WPF pick the right one according to the current view model. That would look something like this:
<UserControl x:Class="TestSO46736914MissingProperty.UserControl1"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:l="clr-namespace:TestSO46736914MissingProperty"
mc:Ignorable="d"
Content="{Binding}"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate DataType="{x:Type l:ViewModel1}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
<CheckBox IsChecked="{Binding Path=Always}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
<StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
<CheckBox IsChecked="{Binding Path=Sometimes}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type l:ViewModel2}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
<CheckBox IsChecked="{Binding Path=Always}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
</Grid>
</DataTemplate>
</UserControl.Resources>
</UserControl>
I.e. just set the Content of your UserControl object to the view model object itself, so that the appropriate template is used to display the data in the control. The template for the view model object that doesn't have the property, doesn't reference that property and so no warning is generated.
Yet another option, which like the above also addresses your concern about the displayed warning, is to create a "shim" (a.k.a. "adapter") object that mediates between the unknown view model type and a consistent one the UserControl can use. For example:
class ViewModelWrapper : NotifyPropertyChangedBase
{
private readonly dynamic _viewModel;
public ViewModelWrapper(object viewModel)
{
_viewModel = viewModel;
HasSometimes = viewModel.GetType().GetProperty("Sometimes") != null;
_viewModel.PropertyChanged += (PropertyChangedEventHandler)_OnPropertyChanged;
}
private void _OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_RaisePropertyChanged(e.PropertyName);
}
public bool Always
{
get { return _viewModel.Always; }
set { _viewModel.Always = value; }
}
public string OnOffAlways
{
get { return _viewModel.OnOffAlways; }
set { _viewModel.OnOffAlways = value; }
}
public bool Sometimes
{
get { return HasSometimes ? _viewModel.Sometimes : false; }
set { if (HasSometimes) _viewModel.Sometimes = value; }
}
public string OnOffSometimes
{
get { return HasSometimes ? _viewModel.OnOffSometimes : null; }
set { if (HasSometimes) _viewModel.OnOffSometimes = value; }
}
private bool _hasSometimes;
public bool HasSometimes
{
get { return _hasSometimes; }
private set { _UpdateField(ref _hasSometimes, value); }
}
}
This object uses the dynamic feature in C# to access the known property values, and uses reflection on construction to determine whether or not it should try to access the Sometimes (and related OnOffSometimes) property (accessing the property via the dynamic-typed variable when it doesn't exist would throw an exception).
It also implements the HasSometimes property so that the view can dynamically adjust itself accordingly. Finally, it also proxies the underlying PropertyChanged event, to go along with the delegated properties themselves.
To use this, a little bit of code-behind for the UserControl is needed:
partial class UserControl1 : UserControl, INotifyPropertyChanged
{
public ViewModelWrapper ViewModelWrapper { get; private set; }
public UserControl1()
{
DataContextChanged += _OnDataContextChanged;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
private void _OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ViewModelWrapper = new ViewModelWrapper(DataContext);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModelWrapper)));
}
}
With this, the XAML is mostly like what you originally had, but with a style applied to the optional StackPanel element that has a trigger to show or hide the element according to whether the property is present or not:
<UserControl x:Class="TestSO46736914MissingProperty.UserControl1"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:l="clr-namespace:TestSO46736914MissingProperty"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid DataContext="{Binding ViewModelWrapper, RelativeSource={RelativeSource AncestorType=UserControl}}">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="40"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffAlways}"/>
<CheckBox IsChecked="{Binding Path=Always}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
<StackPanel Grid.Row="3" Orientation ="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,5,0,5">
<StackPanel.Style>
<p:Style TargetType="StackPanel">
<p:Style.Triggers>
<DataTrigger Binding="{Binding HasSometimes}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</StackPanel.Style>
<TextBlock Margin="5,0,5,0" Text="{Binding Path=OnOffSometimes}"/>
<CheckBox IsChecked="{Binding Path=Sometimes}">
<TextBlock Text="On/Off"/>
</CheckBox>
</StackPanel>
</Grid>
</UserControl>
Note that the top-level Grid element's DataContext is set to the UserControl's ViewModelWrapper property, so that the contained elements use that object instead of the view model assigned by the parent code.
(You can ignore the p: XML namespace…that's there only because Stack Overflow's XAML formatting gets confused by <Style/> elements that use the default XML namespace.)
While I in general would prefer the template-based approach, as the idiomatic and inherently simpler one, this wrapper-based approach does have some advantages:
It can be used in situations where the UserControl object is declared in an assembly different from the one where the view model types are declared, and where the latter assembly cannot be referenced by the former.
It removes the redundancy that is required by the template-based approach. I.e. rather than having to copy/paste the shared elements of the templates, this approach uses a single XAML structure for the entire view, and shows or hides elements of that view as appropriate.
For completeness, here is the NotifyPropertyChangedBase class used by the ViewModelWrapper class above:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
_RaisePropertyChanged(propertyName);
}
protected void _RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
For what it's worth, I prefer this approach to re-implementing the INotifyPropertyChanged interface in each model object. The code is a lot simpler and easier to write, simpler to read, and less prone to errors.
Here's a fairly simple solution using DataTriggers and a custom converter:
<Style TargetType="CheckBox">
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={HasPropertyConverter PropertyName=Sometimes}}" Value="True">
<Setter Property="IsChecked" Value="{Binding Sometimes}" />
</DataTrigger>
</Style.Triggers>
</Style>
The converter:
public class HasPropertyConverter : IValueConverter
{
public string PropertyName { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(PropertyName))
return DependencyProperty.UnsetValue;
return value?.GetType().GetProperty(PropertyName) != null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class HasPropertyConverterExtension : MarkupExtension
{
public string PropertyName { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
=> new HasPropertyConverter { PropertyName = PropertyName };
}
Related
This question already has answers here:
How to pass data from MainWindow to a User Control that's inside the MainWindow?
(1 answer)
Issue with DependencyProperty binding
(3 answers)
Closed 1 year ago.
I am on practice using WPF MVVM with a YouTube video. I made menu buttons in a main window and UserControls, and when I click a menu the UserControl is shown in the main window. The MainViewModel has each ViewModel of the UserControl, and the shown UserControl is changed by changing CurrentView in the MainViewModel.
I want to set the DataContext in the UserControl before it is initialized, but I do not know how. I tried to change ViewModel member values in the MainViewModel, but the actual DataContext member variables in the UserControl are just null.
What I want to ask is how I can set the DataContext in the UserControl when it initialize. Or is there any way to access DataContext from MainWindow?
My Code is below:
Application.xaml
<Application x:Class="Practice.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Practice"
xmlns:viewModel="clr-namespace:Practice.MVVM.ViewModel"
xmlns:view="clr-namespace:Practice.MVVM.View"
Startup="Application_Startup">
<Application.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModel:HomeViewModel}">
<view:HomeView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:UserViewModel}">
<view:UserView/>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
MainWindow.xaml
<Window x:Class="Practice.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Practice"
xmlns:viewModel="clr-namespace:Practice.MVVM.ViewModel"
xmlns:view="clr-namespace:Practice.MVVM.View"
mc:Ignorable="d"
Title="Practice" MinHeight="800" MinWidth="1200"
WindowStyle="None"
ResizeMode="NoResize"
Background="Transparent"
AllowsTransparency="True"
Loaded="Window_Loaded">
<Window.DataContext>
<viewModel:MainViewModel/>
</Window.DataContext>
<Border Background="#002241"
CornerRadius="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="Logo"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
Foreground="White"/>
<Grid Grid.Row="1" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="75"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<RadioButton Content="Home"
Height="50"
Foreground="White"
FontSize="16"
IsChecked="True"
Command="{Binding HomeViewCommand}"/>
<RadioButton Content="Users"
Height="50"
Foreground="White"
FontSize="16"
Command="{Binding UserViewCommand}"/>
</StackPanel>
</Grid>
<ContentControl Grid.Row="1" Grid.Column="1"
x:Name="CC_View"
Margin="10"
Content="{Binding CurrentView}"/>
</Grid>
</Border>
</Window>
HomeView.xaml
<UserControl x:Class="Practice.MVVM.View.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Practice.MVVM.View"
xmlns:viewModel="clr-namespace:Practice.MVVM.ViewModel"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<viewModel:HomeViewModel/>
</UserControl.DataContext>
<Grid>
</Grid>
</UserControl>
ObservableObject.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Practice.Core
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
RelayCommand.cs
using System;
using System.Windows.Input;
namespace Practice.Core
{
public class RelayCommand:ICommand
{
private Action<object> _execute;
private Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
}
MainViewModel.cs
namespace Practice.MVVM.ViewModel
{
public class MainViewModel : ObservableObject
{
public RelayCommand HomeViewCommand { get; set; }
public RelayCommand UserViewCommand { get; set; }
public HomeViewModel HomeVM { get; set; }
public UserViewModel UserVM { get; set; }
private object _currentView;
private object parameter1;
private object parameter2;
private object parameter3;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
parameter1 = new object();
parameter2 = new object();
parameter3 = new object();
HomeVM = new HomeViewModel();
HomeVM.Initialize(parameter1, parameter2, parameter3);
UserVM = new UserViewModel();
CurrentView = HomeVM;
HomeViewCommand = new RelayCommand(o =>
{
CurrentView = HomeVM;
});
UserViewCommand = new RelayCommand(o =>
{
CurrentView = UserVM;
});
}
}
}
HomeViewModel.cs
namespace Practice.MVVM.ViewModel
{
public class HomeViewModel : ObservableObject
{
private object _parameter1;
private object _parameter2;
private object _parameter3;
public HomeViewModel()
{
}
public void Initialize(object parameter1, object parameter2, object parameter3)
{
_parameter1= parameter1;
_parameter2= parameter2;
_parameter3= parameter3;
}
}
}
Thank you in advance.
I have a Main window named "wpfMenu" With a status bar which contains a text block and progress bar. The status bar needs to be updated from methods which are running on separate windows launched from the Main window (only one window open at any time).
Preferably I would like to pass the min, max, progress, text values to a class called "statusUpdate" to update the progress but i have no idea where to begin and any examples of updating progress bars I've come across are running on the same window.
Here is my code for the Status bar so far
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Custom="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon" x:Class="Mx.wpfMenu"
Title="Mx - Menu" Height="600" Width="1000" Background="#FFF0F0F0" Closed="wpfMenu_Closed">
<Grid>
<StatusBar x:Name="sbStatus" Height="26" Margin="0,0,0,0" VerticalAlignment="Bottom" HorizontalAlignment="Stretch">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem>
<TextBlock Name="sbMessage" Text="{Binding statusUpdate.Message}"/>
</StatusBarItem>
<StatusBarItem Grid.Column="1">
<ProgressBar Name="sbProgress" Width="130" Height="18" Minimum="0" Maximum="100" IsIndeterminate="False"/>
</StatusBarItem>
</StatusBar>
</Grid>
The code for my class is
public class statusUpdate : INotifyPropertyChanged
{
private string _message;
public event PropertyChangedEventHandler PropertyChanged;
public statusUpdate()
{
}
public statusUpdate (string value)
{
this._message = value;
}
public string Message
{
get { return _message; }
set
{
_message = value;
OnPropertyChanged("Message");
}
}
void OnPropertyChanged(string _message)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(_message));
}
}
}
There are several steps to this, but they're all well documented elsewhere. It might seem like a complex process, but it's something you'll do over and over in WPF.
You're right to store all the settings in a class. However this class needs to implement INotifyPropertyChanged and raise a PropertyChanged event in every property setter.
using System.ComponentModel;
public class StatusUpdate : INotifyPropertyChanged
{
private string message;
public event PropertyChangedEventHandler PropertyChanged;
public StatusUpdate()
{
}
public string Message
{
get { return this.message; }
set
{
this.message = value;
this.OnPropertyChanged("Message");
}
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then you can make it a public property of your code-behind class, and bind your progress bar properties to it.
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public StatusUpdate Status { get; set; } = new StatusUpdate();
private void PlayCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
public void PlayCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.Status.Message = "Play";
}
public void StopCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.Status.Message = "Stop";
}
}
Then you can pass a reference to the same class to the child forms, and when they set any of the properties, WPF will catch the event and update the GUI.
Let me know if you can't find an example for any of those steps.
Here's a version of your XAML with the binding and buttons I used for the above example:
<Window x:Class="WpfProgressBar.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfProgressBar"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
<CommandBinding Command="Play" Executed="PlayCommand_Executed" CanExecute="PlayCommand_CanExecute" />
<CommandBinding Command="Stop" Executed="StopCommand_Executed" />
</Window.CommandBindings>
<Grid>
<StackPanel>
<Button Content="Play" Command="Play" />
<Button Content="Stop" Command="Stop" />
</StackPanel>
<StatusBar Height="26" Margin="0,0,0,0" VerticalAlignment="Bottom" HorizontalAlignment="Stretch">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem>
<TextBlock Text="{Binding Status.Message, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, NotifyOnSourceUpdated=True}"/>
</StatusBarItem>
<StatusBarItem Grid.Column="1">
<ProgressBar Width="130" Height="18" Minimum="0" Maximum="100" IsIndeterminate="False"/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
NB, I wouldn't normally do command bindings like this, but didn't want to get into the complications of adding RelayCommand
I used the following answer to get a new window and be able to use the values in another ViewModel: https://stackoverflow.com/a/15512972/3793542
I implemented this to my application, where a user can add a new Customer. To do this, he/she clicks the button in the CustomerDetailsView, and a new window opens (CustomerAddView). Then he/she fill out the details and clicks the button in the window.
Now, the Customer is added, my ListBox updates just fine. But the window doesn't close.
I was hoping that any of you can spot what's wrong, as I can't seem to be able to figure it out and it's been driving me crazy.
The code
The OpenCloseWindowBehavior as mentioned in the linked answer is the same, only renamed to WindowBehavior.
Here's the view where the button is, CustomerDetailsView (shortened a bit):
<UserControl x:Class="QRM.View.CustomerDetailsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:src="clr-namespace:QRM"
xmlns:view="clr-namespace:QRM.View"
xmlns:viewmodel="clr-namespace:QRM.ViewModel"
xmlns:helpers="clr-namespace:QRM.Helpers">
<UserControl.DataContext>
<viewmodel:CustomerDetailsViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<ResourceDictionary Source="../StylesRD.xaml" />
</UserControl.Resources>
<i:Interaction.Behaviors>
<!-- TwoWay binding is necessary, otherwise after user closed a window directly, it cannot be opened again -->
<helpers:WindowBehavior WindowType="view:CustomerAddView" Open="{Binding AddCustomerOpen, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="4*"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid Margin="0,5,0,0">
<!-- Grid with all the details-->
</Grid>
<Line Grid.Row="1" Style="{StaticResource horizontalLineStyle}" />
<StackPanel Grid.Row="2" Orientation="Vertical">
<Button Command="{Binding AddCommand}" CommandParameter="True"> Add a new customer</Button>
<!-- More buttons-->
</StackPanel>
<Line Grid.Row="3" Style="{StaticResource horizontalLineStyle}" />
<StackPanel Grid.Row="4" Orientation="Vertical">
<!-- More buttons-->
</StackPanel>
</Grid>
</UserControl>
The new window CustomerAddView:
<Window x:Class="QRM.View.CustomerAddView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewmodel="clr-namespace:QRM.ViewModel"
Title="Add Customer" ResizeMode="NoResize" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen">
<Window.DataContext>
<viewmodel:CustomerAddViewModel />
</Window.DataContext>
<Window.Resources>
<ResourceDictionary Source="../StylesRD.xaml" />
</Window.Resources>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<!-- Form to put in new Customer's details-->
<Button Grid.Row="2" Grid.ColumnSpan="3" Margin="0,50,0,0"
Command="{Binding AddConfirmCommand}">Add this customer</Button>
</Grid>
</Window>
CustomerDetailsViewModel:
public class CustomerDetailsViewModel : INotifyPropertyChanged
{
public bool isSelected = false;
private Nullable<bool> isVisible = new Nullable<bool>();
DBCustomer dbCustomer = new DBCustomer();
#region Constructor
public CustomerDetailsViewModel()
{
Messenger messenger = App.Messenger;
messenger.Register("CustomerSelectionChanged", (Action<Customer>)(param => ProcessCustomer(param)));
}
#endregion
#region Add a Customer
private bool _addCustomerOpen;
public bool AddCustomerOpen { get { return _addCustomerOpen; } set { _addCustomerOpen = value; OnPropertyChanged("AddCustomerOpen"); } }
private RelayCommand addCommand;
public ICommand AddCommand
{
get { return addCommand ?? (addCommand = new RelayCommand(() => AddCustomer())); }
}
private void AddCustomer()
{
this.AddCustomerOpen = true;
}
//After pressing the button in AddView
private RelayCommand addDoneCommand;
public ICommand AddDoneCommand
{
get { return addDoneCommand ?? (addDoneCommand = new RelayCommand(() => AddCustomerDone(), () => !isSelected)); }
}
public void AddCustomerDone()
{
App.Messenger.NotifyColleagues("NewCustomer");
this.AddCustomerOpen = false;
}
#endregion
#region PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
CustomerAddViewModel:
class CustomerAddViewModel : INotifyPropertyChanged
{
private RelayCommand addConfirmCommand;
DBCustomer dbCustomer = new DBCustomer();
public ICommand AddConfirmCommand
{
get { return addConfirmCommand ?? (addConfirmCommand = new RelayCommand(() => AddConfirmCustomer())); }
}
private void AddConfirmCustomer()
{
if (!dbCustomer.Create(newCustomer))
{
return;
}
CustomerDetailsViewModel _closeTheWindow = new CustomerDetailsViewModel();
_closeTheWindow.AddDoneCommand.Execute(false);
}
private Customer newCustomer = new Customer();
public Customer NewCustomer
{
get { return newCustomer; }
set { newCustomer = value; OnPropertyChanged(new PropertyChangedEventArgs("NewCustomer")); }
}
#region PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
#endregion
}
Well you create new instance of CustomerDetailsViewModel here:
CustomerDetailsViewModel _closeTheWindow = new CustomerDetailsViewModel();
_closeTheWindow.AddDoneCommand.Execute(false);
This instance is not related to anything and it's AddCustomerOpen property is not bound to anything either, so setting it has no effect.
So, I tested many answers I found on different topics, but still my WPF app does not update binded data. When I set all Properties before Initializing MainWindow Data are displayed correctly, but I need to select directory, date, etc. before loading the data. Tried to change DataContext in code behind, but IT doesn't work. All the classes used as VieModels have implemented INotifyPropertyChanged interface (but the PropertyChanged values is always null). I'm out of ideas now...
This is XAML code:
<Window x:Class="WpfDataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfDataBinding"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" Name="Logs">
<Window.DataContext>
<local:CustomDataContexts />
</Window.DataContext>
<Grid Name="Logi">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:CustomButton Grid.Column="0" Grid.Row="1" Margin="5" Height="35" Width="100" x:Name="Choose" Text="Wybierz:" ImageSource="Resources/choose.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="0" Grid.Row="2" Margin="5" Height="35" Width="100" x:Name="Load" Text="Załaduj:" ImageSource="Resources/load.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="0" Grid.Row="3" Margin="5" Height="35" Width="100" x:Name="Search" Text="Szukaj:" ImageSource="Resources/search.png" Click="CustomButton_Click" />
<local:CustomButton Grid.Column="3" Grid.Row="2" Margin="5" Height="80" Width="100" x:Name="Next" Grid.RowSpan="2" Text="Dalej:" ImageSource="Resources/next.png" Click="CustomButton_Click" />
<TabControl DataContext="{Binding TextViewModel}" x:Name="tabControl" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding TxtView.Tabs, ElementName=Logs, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate >
<!-- this is the body of the TabItem template-->
<DataTemplate>
<ListView ItemsSource="{Binding EntryViewModels}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Entry.Tag}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<TabControl DataContext="{Binding SingleNode}" x:Name="tabControl2" Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Code-behind looks like this:
public CustomDataContexts DataContexts { get; set; }
public string Path { get; set; }
public Files Files { get; set; }
public MainWindow()
{
/*Path = #"C:\Users\Slawek\Desktop\Logs\logi";
Files = new Files(Path);
Files.NarrowFiles(false, DateTime.MinValue);
var entry = new EntryCollection(Files.SelectedFiles[1], Files.SelectedFiles, null);
TxtView = new TxtViewModel(new List<TxtTabItem>(new[] { new TxtTabItem(entry) }));*/
InitializeComponent();
}
private void CustomButton_Click(object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement) sender;
switch (fe.Name)
{
case "Choose":
var g = new FolderBrowserDialog();
if (g.ShowDialog() == System.Windows.Forms.DialogResult.OK)
Path = g.SelectedPath;
break;
case "Load":
DataContexts = new CustomDataContexts();
Files = new Files(Path);
Files.NarrowFiles(false, DateTime.MinValue);
var entry = new EntryCollection(Files.SelectedFiles[1], Files.SelectedFiles, null);
DataContexts.TextViewModel = new TxtViewModel(new List<TxtTabItem>(new[] { new TxtTabItem(entry) }));
break;
case "Search":
break;
case "Next":
break;
}
}
CustomDataContexts class:
public class CustomDataContexts : INotifyPropertyChanged
{
private TxtViewModel textViewModel;
public XmlViewModel SingleNode { get; set; }
public TxtViewModel TextViewModel
{
get { return textViewModel; }
set { OnPropertyChanged("TextViewModel"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
EntryViewModel:
public class EntryViewModel : INotifyPropertyChanged
{
private SingleEntry entry;
public SingleEntry Entry
{
get { return entry; }
set
{
entry = value;
OnPropertyChanged("Entry");
}
}
public EntryViewModel(SingleEntry entry)
{
Entry = entry;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
TxtViewModel:
public class TxtViewModel :INotifyPropertyChanged
{
private ObservableCollection <TxtTabItem> tabs;
public ObservableCollection<TxtTabItem> Tabs
{
get { return tabs; }
set
{
tabs = value;
OnPropertyChanged("Tabs");
}
}
public TxtViewModel(List<TxtTabItem> items)
{
Tabs = new ObservableCollection <TxtTabItem>();
foreach (var txtTabItem in items)
{
Tabs.Add(txtTabItem);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And finally TxtTabItem class:
public class TxtTabItem : INotifyPropertyChanged
{
public string Header { get; set; }
public ObservableCollection<EntryViewModel> EntryViewModels { get; set; }
public TxtTabItem(EntryCollection collection)
{
Header = collection.Date.ToShortDateString();
EntryViewModels = new ObservableCollection <EntryViewModel>();
foreach (var entry in collection.Entries)
{
EntryViewModels.Add(new EntryViewModel(entry));
}
OnPropertyChanged("EntryViewModels");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I will be very grateful for any suggestions of how to make this code work. I'm pretty new to WPF and still don't know it well enough.
The framework is there, but generally to intialize controls to changes one binds to the ItemsSource property instead of directly to the DataContext.
In general the DataContext is something that provides information about the current item, but when it is null the parent's data context is used up until the page's data context.
The reason for that is a data context will hold a large class of properties and the binding will look (reflect/reflection) at the DataContext for that named property. So by using an ItemsSource one can bind to a specific set of items, while still having a DataContext full of information to allow other items on the control to be bound to other specific properties.
Under MVVM where the VM or view model is that class of properties as mentioned, set the page's data context to that VM and then bind to individual properties on the different controls you have from the page's datacontext.
so
<TabControl DataContext="{Binding TextViewModel}" x:Name="tabControl"
becomes
<TabControl ItemsSource="{Binding TextViewModel}" x:Name="tabControl"
once you set the page's data context to TxtViewModel.
I provide a binding/VM example on my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding.
I got here a template combo box
<ComboBox x:Name="TryCombo" >
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="75"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Id}" Margin="4,0" Visibility="Collapsed" Grid.Column="0"/>
<TextBlock Text="{Binding Path=Name}" Margin="4,0" Grid.Column="1"/>
<Button x:Name="AddButton" Content="Add" Grid.Column="2"/>
</Grid>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
then the ItemsSource:
public void BindComboboxes()
{
itemMgr.Parameters = RetrieveFilter("");
itemMgr.EntityList = itemMgr.RetrieveMany(itemMgr.Parameters);
TryCombo.ItemsSource = itemMgr.EntityList; //collection;
}
the combo box will load this:
My problem is getting the selected item that I had clicked with the AddButton, I want to get the value of text block that was bound to Path=Id, but how?
Should I put x:Name for each of the TextBlocks?
Bind your Button to a ICommand-Property in your ViewModel and pass the selected Item as CommandParameter
I've made a small Demo-Application for you:
The MainWindow.xaml looks like:
<Window x:Class="ComboBoxDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
WindowStartupLocation="CenterScreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Content="Add Item to ComboBox" Margin="5" Command="{Binding AddItemCommand}"/>
<ComboBox Grid.Row="1" x:Name="TryCombo" ItemsSource="{Binding Parameters, UpdateSourceTrigger=PropertyChanged}" Margin="5">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Id}" Margin="4,2" VerticalAlignment="Center" Grid.Column="0"/>
<TextBlock Text="{Binding Path=Name}" Margin="4,2" Grid.Column="1" VerticalAlignment="Center"/>
<Button x:Name="AddButton" Content="Add" Grid.Column="2" VerticalAlignment="Center" Margin="4,2"
Command="{Binding DataContext.ComboBoxItemAddCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
CommandParameter="{Binding}"/>
</Grid>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
Important is the part in the Window-Declaration of: DataContext="{Binding RelativeSource={RelativeSource Self}}" With this you tell the Window that it's DataContext is in the CodeBehind-File. You also can use another File as your DataContext. Than you have to write:
<Window.DataContext>
<loc:MyClassName/>
</Window.DataContext>
This only works if you add xmlns:loc="clr-namespace:YOURPROJECTNAMESPACE" to the Window-Declaration. In the case of my Demo YOURPROJECTNAMESPACE would be ComboBoxDemo.
I've also created a class for the Parameter which is very simple and looks like:
public class Parameter
{
public string Id { get; set; }
public string Name { get; set; }
}
The CodeBehind of the Window (remember: This is the DataContext of the MainWindow.xaml) looks like:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
namespace ComboBoxDemo
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ICommand addItemCommand;
private ICommand comboBoxItemAddCommand;
private ObservableCollection<Parameter> parameters;
public MainWindow()
{
InitializeComponent();
Parameters = new ObservableCollection<Parameter>();
AddItemCommand = new RelayCommand(AddItem);
ComboBoxItemAddCommand = new RelayCommand(ComboBoxItemAdd);
}
private void ComboBoxItemAdd(object parameter)
{
Parameter para = parameter as Parameter;
if (para != null)
{
// Now you can use your Parameter
}
}
public ObservableCollection<Parameter> Parameters
{
get { return parameters; }
set
{
parameters = value;
OnPropertyChanged();
}
}
public ICommand AddItemCommand
{
get { return addItemCommand; }
set
{
addItemCommand = value;
OnPropertyChanged();
}
}
public ICommand ComboBoxItemAddCommand
{
get { return comboBoxItemAddCommand; }
set
{
comboBoxItemAddCommand = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void AddItem(object parameter)
{
Parameters.Add(new Parameter
{
Id = Guid.NewGuid().ToString(),
Name = "Any Name"
});
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And I also created a very useful Helper-Class which you will likely always need if you are using command-binding. This is the class RelayCommand. This class has a constructer which wants an object of type Action<object>. This action later contains what will be executed when you click the button. The seconde optional parameter I will not explain now. The RelayCommand looks like:
using System;
using System.Windows.Input;
namespace ComboBoxDemo
{
public class RelayCommand : ICommand
{
private readonly Action<object> execute;
private readonly Predicate<object> canExecute;
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null )
{
if (execute == null)
throw new ArgumentNullException("execute");
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (canExecute == null)
return true;
return canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
}
So. Now I will explain you the process: Therefor I use the following abbreviations
V = MainWindow.xaml
VM = MainWindow.xaml.cs
The ComboBox in the V has your given Itemtemplate. In the definition of the ComboBox I've added ItemsSource="{Binding Parameters, UpdateSourceTrigger=PropertyChanged}". This tells the ComboBox that it will get it's children from this collection which is located in the VM.
The collection in the VM is an ObservableCollection<Parameter>. The advantage of this type of Collection is, that it implements the ICollectionChanged-Interface and so your V will get updated if there are items added or removed to this collection.
The Button in the V just adds a dummy-Parameter to the ObservableCollection<Parameter>.
With Command="{Binding AddItemCommand}" I tell the Button that it's command-property is bound to the AddItemCommand in the DataContext. In the constructor of the DataContext (MainWindow.xaml.cs) I'm creating this Command and provide the AddItem-Method which will be called if the command is executed.
The Binding of the Button in the DataTemplate must provide a RelativeSource because inside the Template the DataContext is another one. With the RelativeSource I can tell the Button that its Command-Property is bound to a Command which is located in the DataContext of the Window.
I hope this helps you.
If you want to go deeper into the MVVM-Pattern take a look at this Link
One quick way to do it is to simply bind the Tag property of the button to the same property as the TextBlock.
<Button Tag="{Binding Path=Id}" />
Then in the event handler for the button click event you can cast the sender to Button and get the ID from the Tag property.
int id = Convert.ToInt32((sender as Button).Tag);