I have successfully followed the dotnetcurry tutorial on mvvmlight here: http://www.dotnetcurry.com/showarticle.aspx?ID=1037. This shows how to send data from one view to another, using MVVMLight's messaging via EventToCommand. I've used the Northwind database to code my attempts, and can zip and send the solution if that helps.
I have a Window containing a number of views. The left pane of the window displays a list of employees. Selecting an employee opens a) displays the employee detail in a middle pane and b) displays the list of orders processed by that employee in the right pane. What I'm trying to do is make it so that clicking on one of the orders in the list shows order detail information (eg customer, order-line info) in the bottom pane. What happens is clicking on the order makes the data in the order list and the employee detail view disappear.
The view after clicking a row in the Employee table (please excuse the UI, I know it ain't pretty).
The view after clicking one of the rows in the Order List on the right. The whitespace below the view should now be filled with details about the selected order.
Some code:
EmployeeInfo.xaml (Employee List)
<UserControl ...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mvvm="http://www.galasoft.ch/mvvmlight"
DataContext="{Binding Main, Source={StaticResource Locator}}">
...
<DataGrid Grid.Row="2" ItemsSource="{Binding Employees}" x:Name="dgEmployees" IsReadOnly="True" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<mvvm:EventToCommand Command="{Binding SendEmployeeCommand, Mode=OneWay}"
CommandParameter="{Binding ElementName=dgEmployees, Path=SelectedItem}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
EmployeeOrderView.xaml (List of Orders)
<Grid>
<ListView Name="listOrders" ItemsSource="{Binding NewEmployee.Orders}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<mvvm:EventToCommand Command="{Binding SendOrderCommand, Mode=OneWay}"
CommandParameter="{Binding ElementName=listOrders, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListView.View>
<GridView >
<GridViewColumn DisplayMemberBinding="{Binding Path=OrderDate}" Header="Ordered" />
<GridViewColumn DisplayMemberBinding="{Binding Path=Customer.CompanyName}" />
<GridViewColumn DisplayMemberBinding="{Binding Path=ShippedDate}" Header="Shipped" />
</GridView>
</ListView.View>
</ListView>
</Grid>
The Window (MainWindow.xaml) containing the views is just a grid comprised of the following:
<view:EmployeeInfo Grid.Column="0" Grid.Row="0" />
<view:SaveEmployeeView Grid.Column="1" Grid.Row="0" />
<view:EmployeeOrderView Grid.Column="2" Grid.Row="0" />
<view:OrderCustomerView Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1" />
MainViewModel:
[PreferredConstructor]
public MainViewModel(IDataAccessService proxy)
{
serviceProxy = proxy;
Employees = new ObservableCollection<Employee>();
ReadAllCommand = new RelayCommand(GetEmployees);
NewEmployee = new Employee();
SaveCommand = new RelayCommand<Employee>(SaveEmployee);
SearchCommand = new RelayCommand(SearchEmployee);
SendEmployeeCommand = new RelayCommand<Employee>(SendEmployee);
ReceiveEmployee();
SendOrderCommand = new RelayCommand<Order>(SendOrder);
ReceiveOrderMessage();
}
public RelayCommand ReadAllCommand { get; set; }
public RelayCommand<Employee> SaveCommand { get; set; }
public RelayCommand SearchCommand { get; set; }
public RelayCommand<Employee> SendEmployeeCommand { get; set; }
public RelayCommand<Order> SendOrderCommand { get; set; }
// Properties (all properties in the same form as NewEmployee, but boilerplate code
// omitted for (some, at least) brevity)
public Employee NewEmployee
{
get
{
return newEmployee;
}
set
{
if (newEmployee != value)
{
newEmployee = value;
RaisePropertyChanged("NewEmployee");
}
}
}
private Employee newEmployee;
public ObservableCollection<Employee> Employees ...
private ObservableCollection<Employee> employees;
public string EmployeeName ...
private string employeeName;
public Order CurrentOrder...
private Order currentOrder;
void SendEmployee(Employee message)
{
if (message != null)
{
Messenger.Default.Send<MessageCommunicator>(new MessageCommunicator()
{
EmployeeMessage = message
});
}
}
void ReceiveEmployee()
{
if (NewEmployee != null)
{
Messenger.Default.Register<MessageCommunicator>(this, (emp) =>
{
this.NewEmployee = emp.EmployeeMessage;
});
}
}
void SendOrder(Order orderMessage)
{
if (orderMessage != null)
{
Messenger.Default.Send<MessageCommunicator>(new MessageCommunicator()
{
OrderMessage = orderMessage
});
}
}
void ReceiveOrderMessage()
{
if (CurrentOrder != null)
{
Messenger.Default.Register<MessageCommunicator>(this, (ord) =>
{
this.CurrentOrder = ord.OrderMessage;
});
}
}
I have debugged as far as showing that the Order Message is being sent correctly, but after that it jumps into assembly code and I'm a bit lost (I think it's looking for mvvmlight sources).
To me, it feels like a gui event is causing this to happen - I originally thought it was me not using mvvmlight's eventtocommand properly, but now I am not sure. Can anyone explain why the data disappears or where it has gone?
Related
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"
I have an WPF application using MVVM.
I have a DataGrid bound to an ObservableCollection and a TextBox bound to the DataGrid SelectedItem, so when I click an item in the DataGrid, the TextBox is populated.
I also have a Button using Command and CommandParameter and using RelayCommand to check if the TextBox is empty and then disabling the Button.
That all works great, if I use UpdateSourceTrigger=PropertyChanged. The thing I don't like is because of the binding, if the user changes the text in the TextBox, the DataGrid record is edited. If the user then changes their mind about changing the record, and clicks somewhere else, the record in the DataGrid still shows the edited text.
What I have tried is using Mode=OneWay on the TextBox binding, which works in that it doesn't update the DataGrid record. After the data is saved to the database, I need to manually refresh the DataGrid to show the changes.
The code I have in my code behind is the DataGrid's SelectionChanged event which sets a property on the ViewModel to the selected item.
So in order to show the new changes, I thought adding a call to my GetCategories again after the changes would work. However when the code executes OnPropertyChanged("ReceivedCategories"), my CurrentCategory property becomes null.
My code:
CategoryModel.cs
public class CategoryModel
{
public int CategoryID { get; set; }
public string Description { get; set; }
readonly SalesLinkerDataContext _dbContext = new SalesLinkerDataContext();
public ObservableCollection<CategoryModel> GetCategories()
{
var result = _dbContext.tblSalesCategories.ToList();
List<CategoryModel> categoriesList = result.Select(item => new CategoryModel
{
CategoryID = item.CategoryID,
Description = item.Description.Trim()
}).ToList();
return new ObservableCollection<CategoryModel>(categoriesList);
}
internal bool UpdateCategory(int id, string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
try
{
var category = (from a in _dbContext.tblSalesCategories
where a.CategoryID == id
select a).FirstOrDefault();
if (category != null)
{
category.Description = description;
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool AddCategory(string description)
{
if (_dbContext.tblSalesCategories.Any(x => x.Description == description))
{
MessageBox.Show("A category with the same name already exists.");
return false;
}
var newCategory = new tblSalesCategory();
newCategory.Description = description;
try
{
_dbContext.tblSalesCategories.InsertOnSubmit(newCategory);
_dbContext.SubmitChanges();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
internal bool DeleteCategory(int id)
{
var result = _dbContext.tblSalesCategories.FirstOrDefault(x => x.CategoryID == id);
try
{
if (result != null)
{
_dbContext.tblSalesCategories.DeleteOnSubmit(result);
_dbContext.SubmitChanges();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
return true;
}
}
CategoriesViewModel.cs
public class CategoriesViewModel : ViewModelBase, IPageViewModel
{
public CategoryModel CurrentCategory = new CategoryModel();
public ObservableCollection<CategoryModel> Categories = new ObservableCollection<CategoryModel>();
public RelayCommand GetCategoriesRelay;
public RelayCommand UpdateCategoryRelay;
public RelayCommand AddCategoryRelay;
public RelayCommand DeleteCategoryRelay;
#region Get Categories Command
public ICommand GetCategoriesCommand
{
get
{
GetCategoriesRelay = new RelayCommand(p => GetCategories(),
p => CanGetCategories());
return GetCategoriesRelay;
}
}
private bool CanGetCategories()
{
return true;
}
private void GetCategories()
{
Categories = CurrentCategory.GetCategories();
ReceivedCategories = Categories;
}
#endregion
#region Update Category Command
public ICommand UpdateCategoryCommand
{
get
{
UpdateCategoryRelay = new RelayCommand(p => UpdateCategory((string) p),
p => CanUpdateCategory());
return UpdateCategoryRelay;
}
}
public bool CanUpdateCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
public void UpdateCategory(string description)
{
if (CurrentCategory.UpdateCategory(CurrentCategory.CategoryID, description))
{
GetCategories();
}
}
#endregion
#region Add Category Command
public ICommand AddCategoryCommand
{
get
{
AddCategoryRelay = new RelayCommand(p => AddCategory((string) p),
p => CanAddCategory());
return AddCategoryRelay;
}
}
private bool CanAddCategory()
{
return !String.IsNullOrWhiteSpace(Description);
}
private void AddCategory(string description)
{
if (CurrentCategory.AddCategory(description))
GetCategories();
}
#endregion
#region Delete Category Command
public ICommand DeleteCategoryCommand
{
get
{
DeleteCategoryRelay = new RelayCommand(p => DeleteCategory((int) p),
p => CanDeleteCategory());
return DeleteCategoryRelay;
}
}
private bool CanDeleteCategory()
{
return true;
}
private void DeleteCategory(int id)
{
if (CurrentCategory.DeleteCategory(id))
GetCategories();
}
#endregion
/// <summary>
/// Describes the name that will be used for the menu option
/// </summary>
public string Name
{
get { return "Manage Categories"; }
}
public string Description
{
get
{
return CurrentCategory.Description;
}
set
{
CurrentCategory.Description = value;
OnPropertyChanged("Description");
}
}
public ObservableCollection<CategoryModel> ReceivedCategories
{
get { return Categories; }
set
{
Categories = value;
OnPropertyChanged("ReceivedCategories");
}
}
}
CategoryView.xaml
<UserControl x:Class="SalesLinker.CategoriesView"
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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="600" Background="White">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding GetCategoriesCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="45"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Margin="20,0,0,0" FontSize="20" HorizontalAlignment="Center" Content="Categories"/>
<DataGrid x:Name="LstCategories" Grid.Column="0" Grid.Row="1" AutoGenerateColumns="false"
ItemsSource="{Binding Path=ReceivedCategories, Mode=TwoWay}" SelectionChanged="Selector_OnSelectionChanged"
HorizontalScrollBarVisibility="Disabled" GridLinesVisibility="None"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True" Background="White">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Description}" IsReadOnly="True" Header="Description" Width="300" />
</DataGrid.Columns>
</DataGrid>
<Button Command="{Binding AddCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,20,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}">
<Image Source="/Images/Plus.png"/>
</Button>
<Button Command="{Binding DeleteCategoryCommand}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Top" Height="50" Width="50" Margin="0,75,0,0" Background="Transparent" BorderThickness="0" BorderBrush="Transparent"
CommandParameter="{Binding SelectedItem.CategoryID, ElementName=LstCategories, Mode=OneWay }">
<Image Source="/Images/Minus.png"/>
</Button>
<Grid Grid.Row="1" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" Content="Description:"/>
<TextBox x:Name="TbDescription" DataContext="CategoryModel" Grid.Row="0"
Grid.Column="1" Width="250" Height="Auto" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="10,0,0,0"
Text="{Binding SelectedItem.Description, ElementName=LstCategories, Mode=OneWay}"/>
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Margin="10,0,0,0"
Height="20" Width="120" Content="Update Description"
Command="{Binding UpdateCategoryCommand}"
CommandParameter="{Binding ElementName=TbDescription, Path=Text}" />
</Grid>
</Grid>
And I have also just noticed that using Mode=OneWay on the TextBox breaks my CanExecute pieces of code as well.
So all I can think of is either:
Find a way to bind another property to the TextBox as well?
Find a way of using UpdateSourceTrigger=PropertyChanged on the TextBox, but prevent the DataGrid being updated.
Any ideas?
For the null value in the Observable List you have to update the list with a copy of it and an Dispatcher.Invoke otherwise the gui will crash or show null I had a similar problem on update a observable list in a thread.
So you should write your changes in a list like:
//Your Copy List
ObservableCollection<CategoryModel> _ReceivedCategories;
//Your Execute command for the Gui
public void onUpdateExecuted(object parameter){
Dispatcher.Invoke(new Action(() => ReceivedCategories = new ObservableCollection <CategoryModel> (_ReceivedCategories));
}
//Your Undo Command if the Inputs are not ok
public void onUndoExecuted(object parameter){
Dispatcher.Invoke(new Action(() => _ReceivedCategories = new ObservableCollection <CategoryModel> (ReceivedCategories));
}
//Your Command on every input which is set
public void onInputExecuted(object parameter){
_ReceivedCategories.add(Your Object);
}
And you can add an update Command for the Main List so you first update the Copy List with the values and after an Command set the Copy Collection in the Main Collection
Hope that helps
I couldn't get the above suggestion by SeeuD1 to work, but have have figured out what the problem was.
I have The DataGrid's ItemsSource bound to ReceivedCategories and in the code behind, the SelectionChanged event to use to bind to the TextBox.
After the database save, I call my GetCategories() again to refresh the data.
So when ReceivedCategories gets the data again, the DataGrid's ItemsSource is updated, but not before the SelectionChange event fires trying to set my CurrentCategory to the SelectedItem. However there is no longer anything selected in the DataGrid (index is -1), so assignment fails and sets CurrentCategory to null.
Fix is to simply only assign CurrentCategory to the SelectedItem if something is selected.
Old code:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Fixed:
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var viewmodel = (CategoriesViewModel)DataContext;
if (LstCategories.SelectedIndex > -1)
viewmodel.CurrentCategory = LstCategories.SelectedItems.Cast<CategoryModel>().FirstOrDefault();
}
Thanks for the suggestions.
Currently I have a datagrid and a combobox. I can't seem to get my datagrid to refresh on selectionchanged of my combobox. I'm getting the value (_mySelectedValue) but my datagrid never loads. I tried aDataGrid.refresh.
The grid should load when I select the Combobox.Name, which gives me the ID in _mySelectedID, which my query then uses to query the database and refill my collection. (I think..)
ViewModel
public class UserViewModel : ViewModelBase, IUserViewModel
{
public DataEntities _context = new DataEntities();
public ObservableCollection<UserChoice> userChoices { get; set; }
public ObservableCollection<CompanyName> companyNames { get; set; }
public ObservableCollection<UserTable> userTables { get; set; }
public UserViewModel()
{
GetID();
GetDataAsync();
}
public async Task GetDataAsync()
{
var serv = (from s in _context.UserChoices
join sa in _context.CompanyNames on s.CompanyID equals sa.CompanyID
where sa.CompanyID == mySelectedItem //crap left this out
select s).ToList();
userChoices = new ObservableCollection<UserChoice>(serv);
}
public async Task GetID()
{
var data = _context.CompanyNames.OrderBy(o => o.CompanyID).ToList();
CompanyNames = new ObservableCollection<CompanyName>(data);
}
public byte _mySelectedItem;
public Byte MySelectedItem
{
get { return _mySelectedItem; }
set
{
_mySelectedItem = value;
OnPropertyChanged("MySelectedItem");
}
}
}
XAML
<Grid Margin="0,0,-200,0">
<DataGrid x:Name="aDataGrid"
ItemsSource="{Binding userChoices, UpdateSourceTrigger=PropertyChanged}"
RowDetailsVisibilityMode="VisibleWhenSelected"
EnableRowVirtualization="True" AutoGenerateColumns="False"
HorizontalAlignment="Left" Height="159"
VerticalAlignment="Top" Width="337"
Margin="156,57,0,0">
<DataGrid.Columns>
<DataGridTextColumn x:Name="actChoice" Width="*" Header="Choice"
Binding="{Binding Choice, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<DataGridTextColumn x:Name="actDescription" Width="*" Header="Description"
Binding="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
<Grid x:Name="grid1" HorizontalAlignment="Left" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="Company Name:" Grid.Column="0" HorizontalAlignment="Left" Margin="3" Grid.Row="0" VerticalAlignment="Center"/>
<ComboBox x:Name="companyNameComboBox" Grid.Column="1"
DisplayMemberPath="CompanyName"
HorizontalAlignment="Left" Height="Auto"
ItemsSource="{Binding companyNames}"
Margin="3" Grid.Row="0"
VerticalAlignment="Center" Width="120"
SelectedValuePath="CompanyID"
SelectedValue="{Binding Path= MySelectedItem, Mode= TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="2">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</Grid>
</Grid>
You need to call GetDataAsync() inside your setter for MySelectedItem, and then make userChoices raise a property changed event.
Also, you aren't awaiting any calls in your async methods, so you shouldn't make them async.
public void GetData()
{
// Also remember to plug in the correct company ID!
// I removed the join, too, because it looks like you don't need it.
// If you actual query is more complicated then feel free to add it back.
var serv = (from s in _context.UserChoices
where s.CompanyID == MySelectedItem
select s).ToList();
userChoices = new ObservableCollection<UserChoice>(serv);
}
public void GetID()
{
var data = _context.CompanyNames.OrderBy(o => o.CompanyID).ToList();
CompanyNames = new ObservableCollection<CompanyName>(data);
}
private ObservableCollection<UserChoice> _userChoices;
public ObservableCollection<UserChoice> userChoices
{
get { return _userChoices; }
set
{
_userChoices= value;
OnPropertyChanged("userChoices ");
}
}
private byte _mySelectedItem;
public Byte MySelectedItem
{
get { return _mySelectedItem; }
set
{
_mySelectedItem = value;
GetData();
OnPropertyChanged("MySelectedItem");
}
}
Edit
To make a save method (based on your comment) I would use an ICommand since you have a view model. If you do this, you will first need to install the Prism.Mvvm NuGet package to get the DelegateCommand class (in the Microsoft.Practices.Prism.Commands namespace).
Then add this code to your view model:
public void Save()
{
var context = new AADataEntities();
// Make changes to the context here...
context.SaveChanges();
}
private ICommand _saveCommand = new DelegateCommand(Save);
public ICommand SaveCommand
{
get { return _saveCommand; }
}
Now wire up the save command in the xaml like this:
<Button Content="Save" Command="{Binding SaveCommand}" />
Independent of code incoherence pointed by #Jevans92, I can see one problem, which is the SelectedValuePath + SelectedValue combination on Combobox.
You see, it only looks up for the selected value and updates it visual, but it never updates de binded SelectedValue back. So, if you have a pre-set SelectedValue and load the UserControl, then the Combobox looks up for the correct value to show, but if you change selection, it does not update the SelectedValue object bound to the control.
Long story short: Try removing both SelectedValuePath and SelectedValue and using SelectedItem instead.
If you have to deal only with selecting from the UI to the VM, then you don't event need to call PropertyChanged, as it is only used to notify the UI about changes on the VM.
But, if you need to also set the SelectedItem on the VM for the UI to show, then you will have to implement it.
Also note that, if you are dealing with different instances on your ItemsSource and your SelectedItem, you'd better override Equals + GetHashCode on that class, else it will never be looked up on your SelectedItem.
I have a ListBox;
<ListBox Grid.Row="1" Grid.ColumnSpan="2"
x:Name="customerListBox"
ItemsSource="{Binding Customers}"
DisplayMemberPath="Customername"
SelectionMode="Single" Width="200"/>
Customers is public ObservableCollection<Customer> Customers { get; private set; }
Now I am binding ListBox Selected Item to a Text Box:
<TextBox Text="{Binding ElementName=customerListBox,
Path=SelectedValue.Customername,Mode=OneWay}"/>
I have made it one-way as there I want to commit the changes only on click of my Save button and not when the value change on TextBlock.
<Button Content="Save" Grid.Column="0" Grid.Row="3" Width="80" Height="30"
Command="{Binding SaveCommand}"
You're going the wrong way about it, imho.
Don't bind TextBox directly against selected item. Rather, create a new command, SelectionChangedCommand, and new property, CurrentlyActiveText, bind it against TextBox.
The logic would be simple:
SelectionChangedCommand = new RelayCommand(selectedItem=> {
// todo: ask user if he wants to commit the previous changes?!
CurrentlyActiveText = (string)selectedItem;
})
SaveCommand = new RelayCommand(() => {
yourObservable[SelectedIndex] = CurrentlyActiveText;
});
Perhaps a nicer way of doing this is using Triggers to fire a Command on the ListBox SelectionChanged event. Putting logic in a property setter always feels a bit wrong to me
<ListBox...>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding CustomerListBoxSelectionChanged}" CommandParameter="{Binding ElementName=customerListBox,Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
In your view model:
public Customer CurrentCustomer { get; set; }
public RelayCommand<Customer> CustomerListBoxSelectionChanged { get; set; }
private void OnCustomerListBoxSelectionChanged(Customer customer)
{
CurrentCustomer = customer;
NewCustomerName = customer.CustomerName;
}
private string _newCustomerName;
public string NewCustomerName
{
get { return _newCustomerName; }
set
{
if (_newCustomerName == value)
return;
_newCustomerName = value;
RaisePropertyChanged("NewCustomerName");
}
}
Your TextBox in your XAML becomes:
<TextBox Text="{Binding NewCustomerName}"/>
And finally, your SaveCommand calls a method that simply does...
private void OnSave()
{
CurrentCustomer.CustomerName = NewCustomerName;
}
Note you will also need to make sure that your CustomerName in your Customer object is raising PropertyChanged events in order to reflect the update in your ListBox
Note that doing it this way also saves you a futher lookup the ObservableCollection to perform an update. It'll save you some time - any performance gain is always good :)
This seems so fundamental that I feel I must be missing something in my search for a solution.
I have a ViewModel that has a ObservableCollection< ExcelField> property which is used as a ItemsSource for a ListView in the associated View.
I have added a EventToCommand item in the View which accesses a RelayCommand to pass the Command to a method(ExcelListChanged) in the ViewModel with a parameter which is set to the SelectedItems property in the ViewList.
In my ViewModel would like to construct another List containing the items that have been selected. In the debugger I am able to examine the contents of the object parameter and validate that it indeed holds the information I would like to access. Unfortunately I have not been able to find the key that enables me to access the data in the parameter object, I have tried casting, assigning, ChangeTo and more. The cast and ChangeTo exception with a unknown conversion or something similar. I thought since the object is a ListView.SelectedItems(?) that I might be able to cast it to some flavor of that but it appears that is not doable outside of a ListView object.
A simple solution would be great, a complicated one would be okay and a convoluted one painful.
Thanks for any guidance.
Here are the code pieces
The data structure
public struct ExcelField
{
private int index;
public int Index
{
get { return index; }
private set { index = value; }
}
private string fieldName;
public string FieldName
{
get { return fieldName; }
private set { fieldName = value; }
}
public ExcelField(int ndx, string name)
{
index = ndx;
fieldName = name;
}
}
Pieces from the ViewModel
ObservableCollection<ExcelField> fieldNames;
public ObservableCollection<ExcelField> FieldNames
{
get
{
return fieldNames;
}
set
{
fieldNames = value;
OnPropertyChanged("FieldNames");
}
}
allListChanged = new RelayCommand(args => ExcelListChanged(args));
RelayCommand allListChanged;
public RelayCommand AllListChanged
{
get
{
return allListChanged;
}
}
private void ExcelListChanged(object parameter)
{
var whata = parameter.GetType();
return;
}
And finally the View pieces
<UserControl.Resources>
<ViewModel:ExcelMapperViewModel x:Key="ExcelMapperViewModelDataSource" d:IsDataSource="True"/>
<DataTemplate x:Key="ExcelFieldTemplate">
<StackPanel>
<TextBlock Text="{Binding FieldName, Mode=OneWay}"/>
<TextBlock Text="{Binding Index, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<ListView x:Name="AllFields" Grid.Column="1" Grid.Row="1" Grid.RowSpan="3"
ItemsSource="{Binding FieldNames}"
ItemTemplate="{StaticResource ExcelFieldTemplate}"
>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Index}" Header="Index"/>
<GridViewColumn DisplayMemberBinding="{Binding FieldName}" Header="Field Name"/>
</GridView>
</ListView.View>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<Custom:EventToCommand
Command="{Binding AllListChanged, Mode=OneWay}"
CommandParameter="{Binding ElementName=AllFields, Path=SelectedItems}"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
I set a breakpoint on ExcelListChanged and run the code. When I select items in the ListView ExcelListChanged is triggered and I can examine the parameter object. I am able to see the number of entries selected as a Count on parameter and a array of ExcelField items with the appropriate information for the fields within the structure.
So how do I access the information I can see in the debugger programmatically?
since SelectedItems is an IList http://msdn.microsoft.com/en-us/library/system.collections.ilist(v=vs.110).aspx
you can simply iterate through it...
private void ExcelListChanged(object parameter)
{
IList iConverted= parameter as IList;
if(iConverted!=null){
foreach(YouKnowTheTypeOfElements theElement in iConverted) {
doSomethingWith(theElement);
}
}
return;
}