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 :)
Related
I am new to WPF and have come across a problem.
I have an MVVM WPF application and I want to implement filtering to my DataGrid. I have tried all possible solutions on the internet, but none of them work for me for some reason. I have created a TextBox and binded it to FilterName. What I want it to do is on every keypress, the value of FilterName should be updated and the filter should be triggered. Unfortunately, the filter triggers only once - when I start the application and by putting a breakpoint in the Set block of FilterName, I have discovered that it never reaches it.
Here is the declaration of the TextBox:
<TextBox
x:Name="FilterName"
MinWidth="150"
Margin="{StaticResource SmallTopBottomMargin}"
Background="Transparent"
BorderThickness="0,0,0,1"
Text="{Binding FilterName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, IsAsync=True}"
TextWrapping="Wrap" />
And here is the ViewModel:
private readonly ObservableCollection<PersonData> _data;
public ICollectionView DataCollectionView { get; }
private string _filterName = string.Empty;
public string FilterName
{
get
{
return _filterName;
}
set
{
_filterName = value;
DataCollectionView.Refresh();
}
}
public MainWindowViewModel(ISampleDataService sampleDataService)
{
//Adding the data here
DataCollectionView = CollectionViewSource.GetDefaultView(_data);
DataCollectionView.Filter = FilterByName;
}
private bool FilterByName(object obj)
{
if (obj is PersonData data)
{
return data.Name.Contains(FilterName, StringComparison.InvariantCultureIgnoreCase);
}
return false;
}
This binding should work provided that the view model with the FilterName property is the DataContext of the parent window:
Text="{Binding DataContext.FilterName, UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource AncestorType=Window}}"
Set name of your window to x:Name="_this" and change the TextBox binding:
<TextBox
x:Name="tbFilterName"
DataContext="{Binding ElementName=_this}"
Text="{Binding Path=FilterName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="FilterName_TextChanged"
...
Remove DataCollectionView.Refresh(); call from the FilterName setter, but add
private void FilterName_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
DataCollectionView.Refresh();
}
I am using a Listbox in m WPF app where I tried to remove the SelectedItem so the user can reclick on it to do an action.
I have a classic ListBox :
<ListBox
x:Name="MenuItemList"
Grid.Row="1"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenu, UpdateSourceTrigger=PropertyChanged}">
[...] </ListBox>
I have binded the SelectedMenu in my VM :
public MenuItem SelectedMenu
{
get { return null; }
set
{
MenuIsOpened = false;
DisplayedMenu = value;
OnPropertyChanged("SelectedMenu");
}
}
I tried another way with a private property where I changed it to null
private MenuItem _SelectedMenu;
public MenuItem SelectedMenu
{
get { return _SelectedMenu; }
set
{
MenuIsOpened = false;
DisplayedMenu = value;
_SelectedMenu = null;
OnPropertyChanged("SelectedMenu");
}
}
But it does not work as I want... When I click on an item, the property is null but in the view, the listbox always highlights the selected item and the second click on it doesn't trigger the property.
Heres my working example:
// in constructor or so
AddedBlocks = new ObservableCollection<string>();
// the property
public ObservableCollection<string> AddedBlocks { get; }
/// <summary>
/// Deletes selected block from view and model
/// </summary>
private void OnDeleteSelectedBlock(object parameter)
{
try
{
AddedBlocks.RemoveAt(selectedBlockIndex);
}
}
And XAML:
<ListBox ItemsSource="{Binding Blocks, Mode=OneWay}" SelectedIndex="{Binding SelectedBlockIndex, Mode=TwoWay}">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Delete block" Command="{Binding DeleteSelectedBlock}"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
See the two way binding on the selectedIndex. Maybe its easier to use that instead of selectedItem.
Hope this helps. If you dont want to use the context menu, add a button or this
<KeyBinding Key="Delete" Command="{Binding DeleteSelectedBlock}"/>
I do something else to resolve my problem.
I didn't try to change the selected item but I added a command on my Listbox
<ListBox
x:Name="MenuItemList"
Grid.Row="1"
ItemsSource="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenu, UpdateSourceTrigger=PropertyChanged}">
[...]
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding CloseMenuCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
And in my MVVM, I close my menu as I want to do it at the beginning :
private void ExecuteCloseMenuCommand(object o)
{
MenuIsOpened = false;
}
In this way, the user can reclick on the item menu which is already selected and the menu will still be closed by the click.
I wnat to make a kind of search textbox when the user would input some text to the texbox it will search for him, therefore I want to know what the textbox content, for now I have:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"
...
<TextBox Text="{Binding Search}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyDown">
<cmd:EventToCommand Command="{Binding SearchCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
At the viewModel:
public ICommand SearchCommand { get; private set; }
public MyViewModel()
{
SearchCommand = new RelayCommand(SearchMethod);
}
void SearchMethod()
{
if(Search==null)
MessageBox.Show("Search text is null");
}
string search;
public string Search
{
get { return search; }
set
{
Set(() => Search, ref search, value);
RaisePropertyChanged("Search");
}
}
But every time that I input some text to the textbox it shows the message: Search text is null
By default, the binding is evaluated when the TextBox loses focus. Your event is fired anytime a key is pressed.
To change this behavior, you can set the UpdateSourceTrigger in the Binding:
<TextBox Text="{Binding Search, UpdateSourceTrigger=PropertyChanged}" />
my question here is how to know which button is clicked. My buttons are bound to property of type ObservableCollection which contains objects of type Item and I need to use that object in my ViewModel when a button is clicked. Any ideas how to know which button is clicked? I had few ideas, like sending multiple Command Parameters (1.SelectedItems from ListBox 2.The Object from the button) or bind the object from the button to another property of type Item in the ViewModel after the button is clicked in order to use it. Any ideas will be apreciated.
I have this DataTemplate for buttons
<DataTemplate x:Key="ButtonTemplate">
<WrapPanel>
<Button x:Name="OrderButton"
FontSize="10"
Height="80" Width="80"
Content="{Binding Name}"
Command="{Binding OrderCommand,
Source={StaticResource OrderViewModel}}"
CommandParameter="{Binding ElementName=ListBoxUserControl, Path=SelectedItems}">
</Button>
</WrapPanel>
</DataTemplate>
My ViewModel
public class OrderViewModel : ObservableCollection<Order>, INotifyPropertyChanged
{
public CreateOrderCommand CreateOrderCommand { get; set; }
public ObservableCollection<Item> Data { get; set; }
public OrderViewModel()
{
this.CreateOrderCommand = new CreateOrderCommand(this);
DataObservableCollection data= new DataObservableCollection();
Data = data;
}
}
And I populate my buttons like this
<WrapPanel x:Name="OrderButtons">
<ItemsControl ItemTemplate="{StaticResource ButtonTemplate}"
ItemsSource="{Binding Data, Source={StaticResource OrderViewModel}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</WrapPanel>
Simply change the Button.CommandParameter binding to CommandParamter="{Binding}" if you want the data context of the button (i.e. the item from your items source) as the command parameter or,
CommandParameter="{Binding RelativeSource={RelativeSource Self}}" if you want the actual button that was clicked.
First send the Button DataContext using the CommandParameter. To send the SelectedItem of your Listbox you can use
<Listbox SelectedItem="{Binding SelectedItem}"/>
in your Listbox and make a SelectedItem property in your ViewModel.
private YourItemObject mySelectedItem;
public YourItemObject SelectedItem
{
get { return mySelectedItem; }
set
{
value = mySelectedItem
}
Now you can use the SelectedItem in your ViewModel when the Button gets clicket. If you have multiple selections it gets a little bit more tricky ;).
private ButtonClicked(Parameter object)
{
SelectedItem.UsingIt();
if(object is YourButtonDataContext){
YourButtonDataContext.UsingIt();
}
}
Update with MultiSelection:
With Multiselection you have to do your own Listbox.
public class CustomListBox : ListBox
{
public CustomListBox()
{
this.SelectionChanged += CustomListBox_SelectionChanged;
}
void CustomListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
#region SelectedItemsList
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomListBox), new PropertyMetadata(null));
#endregion
}
In the ViewModel you have to have a property with the SelectedItems.
private IList mySelectedData = new List<SelectedDataObject>();
public IList SelectedData
{
get { return mySelectedData ; }
set
{
if (mySelectedData != value)
{
mySelectedData = value;
RaisePropertyChanged(() => SelectedData);
}
}
}
The XAML Looks like this:
<local:CustomListBox ItemsSource="{Binding YourList}" SelectionMode="Extended" SelectedItemsList="{Binding SelectedData, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
...
</local:CustomListBox>
Source for Multiselection in DataGrid is: https://stackoverflow.com/a/22908694/3330227
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?