Untangle EventToCommand on two controls with MVVM Light - c#

Hi am pretty new to WPF and I have started to implement an app using mvvm pattern based on the mvvm light framework. I found it great but I got a problem using two EventToCommand on controls that are supposed to interact together. I am guessing I am doing something wrong... would you help me to find out what exactly?
I have one window with two controls: a combo box which allows the selection of a Name and a TextBox that displays a Caption. A name has a default caption (hardcoded for the example in the constructor of the ViewModel, see below). So when the user select a name the textbox should display the default caption. However, the caption is editable, which means that the user can change the Caption (as long as he does not change the Name again).
In the example below, I have implemented this using MVVM pattern with MVVM Light framework.
The Ok button, is only bound to a command that logs the value in the OutPut window (to see properties values in the ViewModel).
As you will see in the source code comment, the problem comes from the fact that NameSelectionChanged command triggers the CaptionTextChanged command with an "outdated value". For now, I implemented a hacky workaround (not in the code below) by setting a boolean value that ignores the code in CaptionTextChanged when executing the RaisePropertyChanged in NameSelectionChanged but it is not really satisfactory.
the View in XAML
<Window x:Class="TwoControls.MainWindow"
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:vm="clr-namespace:TwoControls"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:DummyViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</Window.Resources>
<Window.DataContext>
<Binding Path="GetViewModel" Source="{StaticResource Locator}" />
</Window.DataContext>
<Grid>
<ComboBox ItemsSource="{Binding ColumnNames}" x:Name="NamesComboBox" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand Command="{Binding NameSelectionChanged}" CommandParameter="{Binding SelectedValue, ElementName=NamesComboBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
<TextBox Text="{Binding Caption, Mode=TwoWay}" Name="CaptionTextBox" HorizontalAlignment="Left" Height="23" Margin="0,45,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<cmd:EventToCommand Command="{Binding CaptionTextChanged}" CommandParameter="{Binding Text, ElementName=CaptionTextBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<Button Content="Ok" Command="{Binding ClickOk}" HorizontalAlignment="Left" Margin="120,170,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
the view model in C#
public class MainViewModel : ViewModelBase
{
private readonly List<string> _names;
private readonly string[] _captions;
public MainViewModel()
{
_names = new List<string>(new[]{"TOTO","TATA","TUTU"});
_captions = new[] {"toto", "tata", "tutu"};
}
public string Name { get; set; }
public string Caption { get; set; }
public ICommand NameSelectionChanged
{
get
{
return new RelayCommand<string>((input) =>
{
Name = input;
int index = _names.IndexOf(input);
Caption = _captions[index];
//Trigger the execution of CaptionTextChanged with the old value of the TextBox
//even if Caption and TextBox.Text are bound TwoWay....
base.RaisePropertyChanged(()=>this.Caption);
});
}
}
public ICommand CaptionTextChanged
{
get
{
return new RelayCommand<string>((input) =>
{
Caption = input;
});
}
}
public ICommand ClickOk
{
get
{
return new RelayCommand(() =>
{
Console.WriteLine("Name=" + Name +";" +"Caption=" + Caption);
});
}
}
public List<string> ColumnNames
{
get { return _names; }
}
}
Ps: targeted .NET is 3.5 and MVVMLight's version is 4.1.27.1

You don't need to use any events to do this in WPF. All this is possible by just using property Bindings if you implement the INotifyPropertyChanged interface on your view model as is customary... how about this:
private ObservableCollection<string> columnNames = new
ObservableCollection<string>();
public ObservableCollection<string> ColumnNames
{
get { return columnNames; }
set { columnNames = value; NotifyPropertyChanged("ColumnNames"); }
}
private string selectedColumnName;
public string SelectedColumnName
{
get { return selectedColumnName; }
set
{
selectedColumnName = value;
NotifyPropertyChanged("SelectedColumnName");
int index = _names.IndexOf(value); // <<< Changes are reflected here
Caption = _captions[index];
}
}
private string caption = string.Empty;
public string Caption
{
get { return caption; }
set { caption = value; NotifyPropertyChanged("Caption"); }
}
In XAML:
<Grid>
<ComboBox ItemsSource="{Binding ColumnNames}" SelectedItem="{Binding
SelectedColumnName}" x:Name="NamesComboBox" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="120" />
<TextBox Text="{Binding Caption, Mode=TwoWay}" Name="CaptionTextBox"
HorizontalAlignment="Left" Height="23" Margin="0,45,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="120" />
<Button Content="Ok" Command="{Binding ClickOk}" HorizontalAlignment="Left"
Margin="120,170,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
Let me know how it goes.

Related

Returning bound checkbox values using MVVM in a WPF form

I have an object that consists of a string and an array. The string populates a ComboBox and the array populates a ListView depending on the selected string value. Each line of the ListViewconsists of a TextBlock and a CheckBox.
On submit I want to be able to verify which items have been selected for further processing but there's a disconnect when using the MVVM approach. I currently have the DataContext of the submit Button binding to the ListView but only the first value is being returned upon submit (somewhere I need to save the selected values to a list I assume but I'm not sure where). I added an IsSelected property to the model which I think is the first step, but after that I've been grasping at straws.
Model
namespace DataBinding_WPF.Model
{
public class ExampleModel { }
public class Example : INotifyPropertyChanged
{
private string _name;
private string[] _ids;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
RaisePropertyChanged("IsSelected");
}
}
}
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
RaisePropertyChanged("Name");
}
}
}
public string[] IDs
{
get => _ids;
set
{
if (_ids != value)
{
_ids = value;
RaisePropertyChanged("IDs");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new
PropertyChangedEventArgs(property));
}
}
}
}
ViewModel
namespace DataBinding_WPF.ViewModel
{
public class ExampleViewModel : INotifyPropertyChanged
{
public ObservableCollection<Example> Examples
{
get;
set;
}
// SelectedItem in the ComboBox
// SelectedItem.Ids will be ItemsSource for the ListBox
private Example _selectedItem;
public Example SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
RaisePropertyChanged(nameof(SelectedItem));
}
}
// SelectedId in ListView
private string _selectedId;
public string SelectedId
{
get => _selectedId;
set
{
_selectedId = value;
RaisePropertyChanged(nameof(SelectedId));
}
}
private string _selectedCheckBox;
public string IsSelected
{
get => _selectedCheckBox;
set
{
_selectedCheckBox = value;
RaisePropertyChanged(nameof(IsSelected));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new
PropertyChangedEventArgs(property));
}
}
public void LoadExample()
{
ObservableCollection<Example> examples = new ObservableCollection<Example>();
examples.Add(new Example { Name = "Mark", IDs = new string[] { "123", "456" }, IsSelected = false });
examples.Add(new Example { Name = "Sally", IDs = new string[] { "789", "101112" }, IsSelected = false });
Examples = examples;
}
/* BELOW IS A SNIPPET I ADDED FROM AN EXAMPLE I FOUND ONLINE BUT NOT SURE IF IT'S NEEDED */
private ObservableCollection<Example> _bindCheckBox;
public ObservableCollection<Example> BindingCheckBox
{
get => _bindCheckBox;
set
{
_bindCheckBox = value;
RaisePropertyChanged("BindingCheckBox");
}
}
}
}
View
<UserControl x:Class = "DataBinding_WPF.Views.StudentView"
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:DataBinding_WPF"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left" >
<ComboBox HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding Path=Examples}"
SelectedItem="{Binding SelectedItem}"
DisplayMemberPath="Name"/>
<ListView x:Name="myListView"
ItemsSource="{Binding SelectedItem.IDs}"
DataContext="{Binding DataContext, ElementName=submit_btn}"
SelectedItem="{Binding SelectedId}"
Height="200" Margin="10,50,0,0"
Width="Auto"
VerticalAlignment="Top"
Background="AliceBlue">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<CheckBox
Name="myCheckBox"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListViewItem}}"
Margin="5, 0"/>
<TextBlock Text="{Binding}" FontWeight="Bold" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button HorizontalAlignment="Left" Height="20" Width="100"
Click="Submit" x:Name="submit_btn">Submit</Button>
</StackPanel>
</Grid>
</UserControl>
View.cs
namespace DataBinding_WPF.Views
{
/// <summary>
/// Interaction logic for StudentView.xaml
/// </summary>
public partial class StudentView : UserControl
{
public StudentView()
{
InitializeComponent();
}
private void Submit(object sender, EventArgs e)
{
var selectedItems = ((Button)sender).DataContext;
// process each selected item
// foreach (var selected in ....) { }
}
}
}
The ListView control already exposes a selected items collection as property SelectedItems.
private void Submit(object sender, RoutedEventArgs e)
{
var selectedIds = myListView.SelectedItems.Cast<string>().ToList();
// ...do something with the items.
}
However, I doubt that you want to do this in the code-behind, but rather in the view model. For this purpose, WPF offers the concept of commands.
MVVM - Commands, RelayCommands and EventToCommand
What you need is a relay command or delegate command (the name varies across frameworks). It encapsulates a method that should be executed for e.g. a button click and a method to determine whether the command can be executed as an object that can be bound in the view. Unfortunately, WPF does not provide an implementation out-of-the-box, so you either have to copy an implementation like here or use an MVVM framework that already provides one, e.g. Microsoft MVVM Tookit.
You would expose a property Submit of type ICommand in your ExampleViewModel and initialize it in the constructor with an instance of RelayCommand<T> that delegates to a method to execute.
public class ExampleViewModel : INotifyPropertyChanged
{
public ExampleViewModel()
{
Submit = new RelayCommand<IList>(ExecuteSubmit);
}
public RelayCommand<IList> Submit { get; }
// ...other code.
private void ExecuteSubmit(IList selectedItems)
{
// ...do something with the items.
var selectedIds = selectedItems.Cast<string>().ToList();
return;
}
}
In your view, you would remove the Click event handler and bind the Submit property to the Command property of the Button. You can also bind the SelectedItems property of the ListView to the CommandParameter property, so the selected items are passed to the command on execution.
<Button HorizontalAlignment="Left"
Height="20"
Width="100"
x:Name="submit_btn"
Command="{Binding Submit}"
CommandParameter="{Binding SelectedItems, ElementName=myListView}">Submit</Button>
Additionally, a few remarks about your XAML.
Names of controls in XAML should be Pascal-Case, starting with a capital letter.
You should remove the DataContext binding from ListView completely, as it automatically receives the same data context as the Button anyway.
DataContext="{Binding DataContext, ElementName=submit_btn}"
You can save yourself from exposing and binding the SelectedItem property in your ExampleViewModel, by using Master/Detail pattern for hierarchical data.
<Grid>
<StackPanel HorizontalAlignment = "Left" >
<ComboBox HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding Path=Examples}"
IsSynchronizedWithCurrentItem="True"
DisplayMemberPath="Name"/>
<ListView ItemsSource="{Binding Examples/IDs}"
SelectedItem="{Binding SelectedId}"
Height="200" Margin="10,50,0,0"
Width="Auto"
VerticalAlignment="Top"
Background="AliceBlue">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<CheckBox Name="myCheckBox"
IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}"
Margin="5, 0"/>
<TextBlock Text="{Binding}"
FontWeight="Bold" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button HorizontalAlignment="Left"
Height="20"
Width="100"
Command="{Binding Submit}"
CommandParameter="{Binding SelectedItems, ElementName=myListView}">Submit</Button>
</StackPanel>
</Grid>
If the view's data context is bound to the view then remove the DataContext from the ListView.
You could remove the item template and instead use a GridView like:
<ListView.View>
<GridView >
<GridViewColumn Header="Selected" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Name}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
Since the ItemSource is an Observable collection, there are several options to monitor changes in the checkboxes:
Add an event handler to the item changed event of the collection and then you can add the Name or the collection index to a local collection. e.g Examples[e.CollectionIndex].Name
Alternatively iterate over the observable collection and select those Examples where Selected = "true"

ObservableCollection of objects of a certain type and treeview binding in WPF

I am trying to bind an ObservableCollection of objects to a TreeView. The object class is shown below
public partial class Layer_Properties : Window
{
//fields
private string _routeName;
private List<Stop> _Stops_List = new List<Stop>();
public string routeName // property
{
get { return _routeName; } // get method
set { _routeName = value; } // set method
}
public List<Stop> Stops_List // property
{
get { return _Stops_List; } // get method
set { _Stops_List = value; } // set method
}
public List<PolylineBarrier> polylineBarriers// property
{
get { return _polylineBarriers; } // get method
set { _polylineBarriers= value; } // set method
}
public Layer_Properties(RouteTask asolveRouteTask, MapViewModel aMapViewModel)
{
InitializeComponent();
solveRouteTask = asolveRouteTask;
_mapViewModel = aMapViewModel;
this.Loaded += async (o, e) =>
{
await Task.Run(() => getnetworkDatasetProprties(solveRouteTask));
routeGUID = Convert.ToString(Guid.NewGuid());
dateTime_label.IsEnabled = false;
dateTime_ComboBox.IsEnabled = false;
this.dateTime_ComboBox.Value = DateTime.UtcNow;
Use_Time_Windows_chkbox.IsEnabled = false;
Use_Time_Windows_chkbox.IsEnabled = false;
Use_Time_Windows_chkbox.IsEnabled = false;
PreserveFirstStop_chkbox.IsChecked = true;
PreserveLastStop_chkbox.IsChecked = true;
PreserveFirstStop_chkbox.IsEnabled = false;
PreserveLastStop_chkbox.IsEnabled = false;
Layer_Properties item = null;
if (_mapViewModel.LayersPool.Count <= 1)
{
item = _mapViewModel.LayersPool[_mapViewModel.LayersPool.Count - 1];
}
else
{
item = _mapViewModel.LayersPool[_mapViewModel.LayersPool.Count - 2];
}
if (item != null)
{
item.routeName = IndexedFilename("Route", item.routeName);
}
};
}
Part of the mapviewmodel is shown below:
public class MapViewModel : INotifyPropertyChanged
{
public MapViewModel()
{
}
//test 09062020
public ObservableCollection<Layer_Properties> LayersPool
{
get { return layersPool; }
set
{
layersPool = value;
NotifiyPropertyChanged("LayersPool");
}
}
private ObservableCollection<Layer_Properties> layersPool= new
ObservableCollection<Layer_Properties>();
void NotifiyPropertyChanged(string property)
{
if (LayersPoolChanged != null)
LayersPoolChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler LayersPoolChanged;
//endtest
}
Part of the xaml is shown below:
<Window x:Class="GIS_App.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:esri="http://schemas.esri.com/arcgis/runtime/2013"
xmlns:local="clr-namespace:GIS_App"
mc:Ignorable="d"
Title="MainWindow" Height="732" Width="1399" Closing="Window_Closing">
<Window.Resources>
<local:MapViewModel x:Key="MapViewModel"/>
<ImageBrush x:Key="NetworkAnalystWindow" ImageSource="/icons/NetworkAnalystWindow.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="AddNetworkElement" ImageSource="/icons/AddNetworkElement_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="solveRoute" ImageSource="/icons/solve_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="solvePremiumRoute" ImageSource="/icons/solvePremium_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="RouteDirections" ImageSource="/icons/directions_btn.png" Stretch="UniformToFill"/>
<ImageBrush x:Key="AddTrafficLayer" ImageSource="/icons/AddTrafficLayer.png" Stretch="UniformToFill"/>
<ContextMenu x:Key="cmButton">
<MenuItem Name ="Draw_Sketch" Header="Draw Sketch" Click="btn_Click"/>
<MenuItem Name ="Save_Sketch" Header="Save Sketch" Click="btn_Click"/>
<MenuItem Name ="Delete_Sketch" Header="Delete Sketch" Click="btn_Click" IsEnabled="True"/>
<MenuItem Name ="Remove_Selected_Vertex" Header="Remove Selected Vertex" Click="btn_Click" IsEnabled="True"/>
<MenuItem Name ="Cancel" Header="Cancel" Click="btn_Click" IsEnabled="True"/>
<Separator />
<MenuItem Header="Address Geocoding" Click="btn_Click" IsEnabled="True"/>
</ContextMenu>
<ContextMenu x:Key="treeViewMenu">
<MenuItem Name ="Delete_TreeViewElement" Header="Delete" Click="btn_Click"/>
<MenuItem Name ="Delete_All_TreeViewElements" Header="Delete All" Click="btn_Click"/>
<MenuItem Name ="Open_Attribute_Table" Header="Open_Attribute_Table" Click="btn_Click" IsEnabled="True"/>
</ContextMenu>
</Window.Resources>
<Grid HorizontalAlignment="Left" Height="Auto" Margin="5,25,0,20" VerticalAlignment="Stretch" Width="155">
<!-- hieracical binding -->
<TextBlock Text="Hierarchical root binding}" Foreground="Red" Margin="10,20,0,0"/>
<TreeView ItemsSource="{Binding LayersPool}" Margin="10" Height="200">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding LayersPool}" DataType="{x:Type local:Layer_Properties}">
<TreeViewItem Header="{Binding routeName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
i ve made numerous attempts to make it work but i cannot find a solution. What i want to achieve is create a treeview as follows:
Route_1
Stops
Stop1
Stop2
Stop3
Polyline Barriers
Barrier 1
Route_2
Stops
Stop1
Stop2
Polyline Barriers
Barrier 1
Barrier2
Edit I suspect there might be an issue with binding since LayersPool observablecollection is a mapviewmodel collection. Any help will be welcome.
I think you need to understand how TreeView binding works.
You can have a look here: wpf treeview binding and also here: https://www.wpf-tutorial.com/treeview-control/treeview-data-binding-multiple-templates/
Generally speaking, if each TreeViewItem ViewModel needs to do something specific (for example once the user click on it, then you need to create your own ViewModel specific class for each TreeViewItem.
You might create an AbstractViewModel as a base class for every TreeViewItem ViewModel that contains basically 2 properties.
Children binded to ItemsSource in the HierarchicalDataTemplate that contains an ObservableCollection of current treeviewitem's leaves
Name binded to your Text property of the TextBlock you placed in your DataTemplate to show the treeview item text.

Binding Viewmodel to View

MainWindow.xaml
<Window x:Class="SDT.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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
xmlns:viewModels="clr-namespace:SDT.ViewModels"
Height="500" Width="700" WindowStyle="None" AllowsTransparency="False" ResizeMode="NoResize" Background="#FF2C2C2C"
TextElement.Foreground="{DynamicResource MaterialDesignBody}" TextElement.FontWeight="SemiBold">
<Window.DataContext>
<viewModels:UserViewModel />
</Window.DataContext>
<Grid>
<TextBox HorizontalAlignment="Left" Height="23" Margin="308,90,0,0" TextWrapping="Wrap" Text = "{Binding Login}" VerticalAlignment="Top" Width="120"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="152,200,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Button Content="Submit" Command="{Binding SubmitLoginDataCommand}" HorizontalAlignment="Left" Margin="567,259,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
MainWindows.cs
public partial class MainWindow : Window
{
UserViewModel userViewModel = new UserViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = userViewModel;
}
}
UserViewmodel
public class UserViewModel : INotifyPropertyChanged
{
private UserService userService = new UserService();
public string _firstName;
public string Login { get; set; }
public void SubmitLoginData(object loginData)
{
userService.CheckUserExist(Login);
}
public ICommand SubmitLoginDataCommand => new RelayCommand(SubmitLoginData, param => true);
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
OnPropertyChanged("FirstName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Hello.
What is wrong with FirstName binding?
Textbox shows nothing.
public string FirstName{} - FirstName here have value in debugging.
I tried without Window.DataContext and only with Text="{Binding FirstName}" but without success.
Login binding working fine.
You need to remove from MainWindow.xaml this part:
<Window.DataContext>
<viewModels:UserViewModel />
</Window.DataContext>
It becouse you have Twice DataContext,
In xaml and in cs so it is not know from where take the data.
I wanted to post a second answer to make a suggestion that I think you'll really like. We can have your OnPropertyChanged event automatically name the property, allowing you to just write "OnPropertyChanged()" to trigger the UI update.
To do this, we're going to use a property called "Caller Member Name" - which does what you'd think - provides the name of the object or property that's called the code!
To use this, we need to add a using statement to the top of your UserViewModel class:
using System.Runtime.CompilerServices;
Then, we will modify your OnPropertyChanged event to use the 'caller member name' unless you specify a specific name. It should look like this now:
private void OnPropertyChanged([CallerMemberName] String name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Now - we will update your property to use the simplified method:
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
OnPropertyChanged();
}
}
}
Lastly - I've recently learned an alternative way for get/set's that i prefer. What you're doing is completely OK and there is no need to change it, but i'd suggest trying it to see how you like it :)
public string FirstName
{
get => _firstName;
set
{
if (_firstName == value) return;
_firstName = value;
OnPropertyChanged();
}
}
Reasons: I find it quicker to press == instead of !=, less brackets. If first name equals value it will simply return (exit). If not, it skips that return! I love that!
Let's test your binding! Let's add a text block to your form, and bind the FirstName property to it. Whatever you enter in the Textbox should be displayed in the textblock if your binding is working correctly.
Your MainWindow.xaml should look something like this:
<Window x:Class="SDT.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:viewModels="clr-namespace:Junk.cats"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TextBox HorizontalAlignment="Left" Height="23" Margin="308,90,0,0" TextWrapping="Wrap" Text = "{Binding Login}" VerticalAlignment="Top" Width="120"/>
<TextBox HorizontalAlignment="Left" Height="23" Margin="152,200,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Button Content="Submit" Command="{Binding SubmitLoginDataCommand}" HorizontalAlignment="Left" Margin="567,259,0,0" VerticalAlignment="Top" Width="75"/>
<TextBlock HorizontalAlignment="Left" Height="32" Margin="140,247,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="346">
<Run Text="First Name: "/>
<Run Text="{Binding Path=FirstName}"/>
</TextBlock>
</Grid>
I expect that this test will work, and you're going to see that you're not having an issue with your get/set properties and UI updates. I believe your issue is now with the 'instance' (copy) of the UserViewModel.
Let's pretend we're working with a printed document. When you use the = new UserService(); assignment, you're printing a fresh copy of our document. If we print a new document and give it to MainWindow.cs (Let's call it "Bob"), AND you then print a new copy in your userService code (Let's call this "Frank") - these are two independent instances / copies of the document.
We need to make this object once, and tell "Bob" and "Frank" to work with the same copy of the object. Don't worry, this is easier than you think, and you'll start getting used to it as you use it.
I'm going to use some STATIC fields to simplify your troubleshooting - you do not need to create a static instance to make this work, but you do need to make sure your instance of the shared class is available to whoever needs it.
Step 1 - Create a new class, let's call it 'Views'.
Step 2 - Make the class public static
Step 3 - Create a Public static userViewModel here:
public static class views
{
public static UserViewModel userViewModel = new UserViewModel();
}
Now - Let's change your MainWindow.cs to use the shared instance of this class:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = views.userViewModel;
}
}
The last thing you need to do - Make your external function work with the same copy of the 'userViewModel'! I don't have that code from you, so I'm pretending your function is called 'YourFunctioNToChangeTheName', and it's located in your 'UserService' class:
public class UserService
{
public void YourFunctionToChangeTheName()
{
views.userViewModel.FirstName = "FRANK";
}
}
The key thing to spot here is that you're not creating a new "UserViewModel" - you're re-using the same instance that the MainWindow.cs is bound to - so the UI is getting a 'property changed notification' now!
Remember, the UserViewModel (class) itself isn't static, we've created a shared / static instance of it that can be accessed from anywhere in your program. I suggested this approach so that you can learn the basics of an instance :)
Good luck!!

Binding a List to a ComboBox in WPF

I'm trying to create a UI with WPF using MVVM, but I'm having a bit of trouble. I want to have a DataGrid with two columns. The first column will be a label, and the second column will have a ComboBox in it. The label column always shows up, but nothing ever shows up in the ComboBox. I'm don't think I'm binding it properly, but I'm not sure how to fix it.
I created a class with the properties that I want to appear in the DataGrid. The label will be the Parameter property and I have a list property called Revisions for the ComboBox.
class RevisionMapModel
{
private string _parameter;
public string Parameter
{
get { return _parameter; }
}
private List<string> _revisions;
public List<string> Revisions
{
get { return _revisions; }
}
private string _selection;
public string Selection
{
get { return _selection; }
set { _selection = value; }
}
public RevisionMapModel(string parameter, List<string> revisions)
{
// set the parameter name and the list of revisions
_parameter = parameter;
_revisions = revisions;
_selection = "";
// add a default revision
_revisions.Add("None");
// attempt to find which revision matches with this parameter
FullRevisionMatch(_parameter, _revisions);
// if a full match isn't found, then try again
if (_selection == "None")
{
PartialRevisionMatch(_parameter, _revisions);
}
}
}
Here is the ViewModel class that serves as the DataContext. The DataGrid is bound to the RevisionMapModels property, which is a list of the previous class.
class RevisionMapViewModel
{
private string _label;
public string Label
{
get { return _label; }
}
private List<RevisionMapModel> _revisionMapModels;
public List<RevisionMapModel> RevisionMapModels
{
get { return _revisionMapModels; }
}
public RevisionMapViewModel(List<Definition> parameters, List<Revision> revisions, string label)
{
// instantiate the list
_revisionMapModels = new List<RevisionMapModel>();
_label = label;
// convert the parameters and revisions to strings
List<string> revisionDescriptions = revisions.Select(r => r.Description).ToList();
List<string> parameterNames = parameters.Select(p => p.Name).ToList();
// create classes for each parameter
List<string> reservedRevisions = new List<string>();
for (int i=0; i< parameterNames.Count; i++)
{
RevisionMapModel model = new RevisionMapModel(parameterNames[i], revisionDescriptions);
// check to ensure this parameter is not mapped to a revision that is already selected
if (reservedRevisions.Contains(model.Selection))
{
// change the selection to none if it's already used
model.Selection = "None";
}
else
{
reservedRevisions.Add(model.Selection);
}
// add it to the list
_revisionMapModels.Add(model);
}
}
}
This is my XAML below. I'm guessing it's having trouble binding to the Revisions property because it's a list. I've tried a number of things, but nothing seems to get this to show up.
<Window x:Class="SheetRevisions.RevisionMapView"
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:SheetRevisions"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" MinWidth="500" MinHeight="500" Width="500">
<Grid>
<DataGrid Margin="20,40,20,45" MinWidth="50" MinHeight="47" ItemsSource="{Binding RevisionMapModels}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Parameter Name" Binding="{Binding Parameter}" Width="Auto"/>
<DataGridComboBoxColumn Header="Revision Description" ItemsSource="{Binding Revisions}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<Label Content="{Binding Label}" HorizontalAlignment="Left" Height="25" Margin="20,10,0,0" VerticalAlignment="Top" Width="412"/>
<Button Content="Cancel" HorizontalAlignment="Right" Margin="0,0,20,10" VerticalAlignment="Bottom" Width="75" IsCancel="True"/>
<Button Content="OK" HorizontalAlignment="Right" Margin="0,0,110,10" VerticalAlignment="Bottom" Width="75" IsDefault="True"/>
</Grid>
</Window>
I'm really new to WPF so any help is appreciated.
As already mentioned here, you need to implement INotifyPropertyChanged in your ViewModel. Something like
RevisionMapViewModel : INotifyPropertyChanged
//...your code here
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Then, RevisionMapModel should be a public class, as I think.
Also, in case you work in Visual Studio, some tips about your incorrect bindings can be visible in Output window.
In addition to the comments above about implimenting INotifyPropertyChanged I also needed to change my XAML to use a DataGridTemplateColumn rather than a DataGridComboBoxColumn. It has something to do with a list not being a valid source as I found out from this post:
https://social.msdn.microsoft.com/Forums/en-US/e14be49f-1c03-420e-8a15-ca98e7eedaa2/how-to-bind-net-4-datagridcomboboxcolumn-to-a-collection-within-a-collection?forum=wpf
Working XAML:
<Window x:Class="SheetRevisions.RevisionMapView"
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:SheetRevisions"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" MinWidth="500" MinHeight="500" Width="500">
<Grid>
<DataGrid Margin="20,40,20,45" MinWidth="50" MinHeight="47" ItemsSource="{Binding RevisionMapModels}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Parameter Name" Binding="{Binding Parameter}" Width="*" IsReadOnly="True"/>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Revisions}" SelectedItem="{Binding Selection}" Width="Auto"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Label Content="{Binding Label}" HorizontalAlignment="Left" Height="25" Margin="20,10,0,0" VerticalAlignment="Top" Width="412"/>
<Button Content="Cancel" HorizontalAlignment="Right" Margin="0,0,20,10" VerticalAlignment="Bottom" Width="75" IsCancel="True"/>
<Button Content="OK" HorizontalAlignment="Right" Margin="0,0,110,10" VerticalAlignment="Bottom" Width="75" IsDefault="True"/>
</Grid>
</Window>
Revised model:
public class RevisionMapModel : INotifyPropertyChanged
{
#region fields
private Logger _logger = LogUtils.LogFactory.GetCurrentClassLogger();
private string _parameter;
public string Parameter
{
get { return _parameter; }
set { }
}
private List<string> _revisions;
public List<string> Revisions
{
get { return _revisions; }
}
private string _selection;
public string Selection
{
get { return _selection; }
set
{
_selection = value;
OnPropertyRaised("Selection");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public RevisionMapModel(string parameter, List<string> revisions)
{
// set the parameter name and the list of revisions
_parameter = parameter;
_revisions = revisions;
_selection = "";
// add a default revision
_revisions.Add("None");
// attempt to find which revision matches with this parameter
FullRevisionMatch(_parameter, _revisions);
// if a full match isn't found, then try again
if (_selection == "None")
{
PartialRevisionMatch(_parameter, _revisions);
}
}
protected void OnPropertyRaised(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

How to clear a ComboBox after research WPF?

I had a research by criteria with Two combobox, it works fine
after the research is finished, I have a button Display All : to reset the combobox to null..and the DataGrid display with all elements ,
The problem that the combobox must be empty when I click on the Button Dispaly All!
Without select an element in combobox(just dispaly the datagrid):I have 6 elements in the datagrid, it is correct..and the combobox are Empty
After select the Search criteria, i have the result correct: (I have just 3 results, it is the correct action)
3 elements picture
When I click on the button Display All:(I have all the elements in datagrid, 6 elements..It is correct) But the Combobox aren't empty!!
6 elements picture
The view:
<Window x:Class="WPFAuthentification.Views.BusinesseventsView"
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" >
<Label Content="Entity Type" Width="128" Grid.Row="1" Grid.ColumnSpan="2"/>
<ComboBox HorizontalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{Binding EntityLevelEnum}"
SelectedItem="{Binding EntityType, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True, TargetNullValue=''}"
Grid.ColumnSpan="2" Grid.Column="1" />
<Button Content="Dislplay all" ToolTip="Display All Business Events"
VerticalAlignment="Top" Command="{Binding Initialize}"
Visibility="{Binding Path=ShowDisplayAllButton, Converter={StaticResource BoolToVis}}" />
<DataGrid ..... />
</Window>
The ViewModel:
class BusinesseventsViewModel : ViewModelBase1
{
private ObservableCollection<BusinessEventClass> businessEventsList;
private RelayCommand<string> initialize;
public RelayCommand<string> Initialize
{
get { return initialize; }
}
public BusinesseventsViewModel()
{
//businessEventsList: to Get all the Business events
businessEventsList = new ObservableCollection<BusinessEventClass>(WCFclient.getAllBusinessEvent());
//Enumeration of Entity Type and Criticality
levelCriticalityEnum = new ObservableCollection<Level_Criticality>(Enum.GetValues(typeof(Level_Criticality)).Cast<Level_Criticality>());
entityLevelEnum = new ObservableCollection<BusinessEntityLevel>(Enum.GetValues(typeof(BusinessEntityLevel)).Cast<BusinessEntityLevel>());
//the Button Display All :
initialize = new RelayCommand<string>(initFunc);
}
//Function of the Button Display All
private void initFunc(object obj)
{
EntityType = null;
OnPropertyChanged("EntityLevelEnum");
Criticality = null;
OnPropertyChanged("Criticality");
}
private string entityType;
public string EntityType
{
get { return entityType; }
set
{
entityType = value;
businessEventsList = filterByCriteria(entityType, criticality);
OnPropertyChanged("BusinessEventsList");
OnPropertyChanged("EntityType");
}
}
//Function of the research :
public ObservableCollection<BusinessEventClass> filterByCriteria(string entityType, string criticality)
{
BusinessEventsList = new ObservableCollection<BusinessEventClass>(WCFclient.getAllBusinessEvent());
ObservableCollection<BusinessEventClass> updatedList = new ObservableCollection<BusinessEventClass>();
if ((entityType == null) && (Criticality == null))
{
updatedList = businessEventsList;
}
if ((entityType != null && entityType != "") && (Criticality != null))
{
updatedList = new ObservableCollection<BusinessEventClass>(BusinessEventsList.Where(a => a.EntityType.ToString().ToLower().Equals(criticality.ToString())
&& a.Critciality.ToString().Equals(criticality.ToString())));
}
}
There must be something wrong with your implementation of the INotifyPropertyChanged interface.
Using GalaSoft.MvvmLight I did this, and works without problem;
Window.cs content:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
DataContext = new TestViewModel();
}
}
public class TestViewModel : ViewModelBase
{
private string _selectedItem;
public RelayCommand Command { get; }
public ObservableCollection<string> ItemsSource { get; }
public string SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, value); }
}
public TestViewModel()
{
Command = new RelayCommand(() => SelectedItem = null);
ItemsSource = new ObservableCollection<string> { "index 0", "index 1", "index 2", "index 3" };
}
}
and my Window.xaml content
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type testClean:TestViewModel}">
<Grid>
<Viewbox>
<TextBlock Foreground="HotPink">just some pink text</TextBlock>
</Viewbox>
<ComboBox Height="50" Width="200" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="20" SelectedIndex="0"
ItemsSource="{Binding ItemsSource}"
SelectedItem="{Binding SelectedItem}"/>
<Button Command="{Binding Command}" Height="50" Width="100" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="250,20,20,20">Reset</Button>
</Grid>
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<ContentControl Content="{Binding}" />
Tested and working as expected (when you put null as SelectedItem the combobox returns empty)

Categories

Resources