How to trigger converter when ObservableCollection is changed in WPF using MVVM? - c#

I have two list like this:
<ListView Name="listView" Height="300" ItemsSource="{Binding Resultados}">
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox
IsChecked="{Binding Path=CommandParameter, RelativeSource={RelativeSource Self}, Converter={StaticResource CheckBoxCeldaConverter}, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
Command="{Binding Path=DataContext.SeleccionarCeldaCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="EDIFICIO" DisplayMemberBinding="{Binding NIVEL.PASILLO.EDIFICIO.NOMBRE}"/>
<GridViewColumn Header="PASILLO" DisplayMemberBinding="{Binding NIVEL.PASILLO.NOMBRE}"/>
<GridViewColumn Header="NIVEL" DisplayMemberBinding="{Binding NIVEL.NOMBRE}"/>
<GridViewColumn Header="CELDA" DisplayMemberBinding="{Binding CELDA.NOMBRE}"/>
<GridViewColumn Header="CLASIFICACIÓN" DisplayMemberBinding="{Binding CELDA_CATEGORIA.NOMBRE}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Command="{Binding Path=DataContext.MostrarDiasVisitasCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding CELDA.CELDA_DIAS_VISITA}"
IsEnabled="{Binding TieneDiasVisita}">
<materialDesign:PackIcon Kind="Eye"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<ListView Name="listView2" Height="300" ItemsSource="{Binding CeldasSeleccionadas}" Grid.Column="1" Margin="20 0 0 0">
<ListView.View>
<GridView>
<GridViewColumn Header="EDIFICIO" DisplayMemberBinding="{Binding NIVEL.PASILLO.EDIFICIO.NOMBRE}"/>
<GridViewColumn Header="PASILLO" DisplayMemberBinding="{Binding NIVEL.PASILLO.NOMBRE}"/>
<GridViewColumn Header="NIVEL" DisplayMemberBinding="{Binding NIVEL.NOMBRE}"/>
<GridViewColumn Header="CELDA" DisplayMemberBinding="{Binding CELDA.NOMBRE}"/>
<GridViewColumn Header="CLASIFICACIÓN" DisplayMemberBinding="{Binding CELDA_CATEGORIA.NOMBRE}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Command="{Binding Path=DataContext.BorrarCeldaCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Delete"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
In the first list, the elements have a CheckBox, if the CheckBox is pressed, the element is added or removed from the second list that it's ItemSource is binded to CeldasSeleccionadas ObservableCollection in the ViewModel. I have a converter in the binding of the isChecked property of the first list that checks if the element is present in the CeldasSeleccionadas ObservableCollection, if is present, put the isChecked property as true. This is because if the user update the View, the selected values keeps checked.
Now, in the second list, the one binded to CeldasSeleccionadas, I have a button to delete the element from CeldasSeleccionadas ObservableCollection. This works fine, the only problem I have is that when I remove the element from CeldasSeleccionadas ObservableCollection the CheckBox in the first list keeps isChecked property as true, the converter is not beign called again and is not checking if the element exist in the CeldasSeleccionadas ObservableCollection.
CeldasSeleccionadas looks like this in the ViewModel:
public ObservableCollection<CeldaModel> CeldasSeleccionadas { get; set; } = new ObservableCollection<CeldaModel>();
This is the Command to add elements to CeldasSeleccionadas:
private ICommand _SeleccionarCeldaCommand;
public ICommand SeleccionarCeldaCommand {
get {
if (_SeleccionarCeldaCommand == null) _SeleccionarCeldaCommand = new RelayCommand(param =>SeleccionarCelda((CeldaModel) param));
return _SeleccionarCeldaCommand;
}
}
void SeleccionarCelda(CeldaModel celda) {
if (CeldasSeleccionadas.Contains(celda)) {
CeldasSeleccionadas.Remove(celda);
}
else {
CeldasSeleccionadas.Add(celda);
}
}
And this for remove elements:
private ICommand _BorrarCeldaCommand;
public ICommand BorrarCeldaCommand {
get {
if (_BorrarCeldaCommand == null) _BorrarCeldaCommand = new RelayCommand(param =>BorrarCelda((CeldaModel) param));
return _BorrarCeldaCommand;
}
}
void BorrarCelda(CeldaModel celda) {
CeldasSeleccionadas.Remove(celda);
}
The Converter looks like this:
public class CheckBoxCeldaConverter : Freezable, IValueConverter
{
public CheckBoxCeldaConverter()
{
MyCollection = new ObservableCollection<CeldaModel>();
}
protected override Freezable CreateInstanceCore()
{
return new CheckBoxCeldaConverter();
}
public static readonly DependencyProperty MyCollectionProperty =
DependencyProperty.Register(nameof(MyCollection),
typeof(ObservableCollection<CeldaModel>), typeof(CheckBoxCeldaConverter),
new FrameworkPropertyMetadata(null));
public ObservableCollection<CeldaModel> MyCollection
{
get { return GetValue(MyCollectionProperty) as ObservableCollection<CeldaModel>; }
set { SetValue(MyCollectionProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (MyCollection.Contains(value as CeldaModel))
{
return true;
}
else
{
return false;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And I call it from my view inside the User.Resources like this:
<UserControl.Resources>
<local:CheckBoxCeldaConverter MyCollection="{Binding CeldasSeleccionadas}" x:Key="CheckBoxCeldaConverter"/>
</UserControl.Resources>
How can I trigger the converter when a item from the CeldasSeleccionadas ObservableCollection is added or removed?

Read all, it's more intuitive and explained than before.
The code will be much easier if CeldaModel was aware of "I'm selected", letting you bind the checkbox to "Selected" boolean property.
With that you can avoid the converter.
But I think there is a better choice.
You have "Resultados".
Then in you ViewModel creates two more properties ICollectionView
public class MasterViewModel : ViewModelBase //example{
ObservableCollection<CeldaModel> Resultados{get; set;}
ICollectionView TodosResultados{get; set;}
ICollectionView ResultadosSeleccionados {get; set;} //You create ICollectionView and filter it by "Selected" property in CeldaModel
...
}
Bind one listview to "TodosResultados" and the other to "ResultadosSeleccionados"
If you have bound everything correctly (checkbox with Selected, ListViews with ICollectionView), you don't have to worry about nothing. Everything will work like a charm. At least in the surface.
No need to create the "Commands" (unless the command make something in the back Model- don't mess up with the ViewModel).
Warning: this work fine if you only want "View", if you want two ObservableCollection is valid too.
PS: should you name "CeldaModel" "CeldaViewModel"? In a pure MVVM, Model classes must not interact in any way with the view, even if you include it in a collection.

Related

How to get a reference of a ListView item to its datatemplates ViewModel which is created as static resource? [duplicate]

I did my research that people tend to use ViewModel to achieve this but I am sort of stuck in it.
I have a
public ObservableCollection<Order> orderList { get; set; } = new ObservableCollection<Order>();
in MainWindow which is already filled up with data.
in MainWindow XAML I have a User Control inside the TabControl:
<TabControl x:Name="TabCollection">
<TabItem Header="UC1">
<local:UserControl1/>
</TabItem>
<TabItem Header="UC2">
<local:UserControl2/>
</TabItem>
</TabControl>
We only talk about UC1 here so in UC1 XAML here I have a ListView inside:
<UserControl.DataContext>
<local:UserControl1VM/>
</UserControl.DataContext>
<ListView x:Name="ListViewText">
<ListView.View>
<GridView>
<GridViewColumn Header="First name" DisplayMemberBinding="{Binding Firstname}"/>
<GridViewColumn Header="Last Name" DisplayMemberBinding="{Binding Lastname}"/>
<GridViewColumn Header="Order" DisplayMemberBinding="{Binding Ordername}"/>
<GridViewColumn Header="Delivery time" DisplayMemberBinding="{Binding Deliverytime}"/>
<GridViewColumn Header="Phone Number" DisplayMemberBinding="{Binding Phone}"/>
<GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address}"/>
<GridViewColumn Header="Email" DisplayMemberBinding="{Binding Email}"/>
</GridView>
</ListView.View>
</ListView>
And here's the code in UserControl1VM.cs:
namespace QuickShop
{
class UserControl1VM : INotifyPropertyChanged
{
private ObservableCollection<Order> orderList;
public ObservableCollection<Order> OrderList
{
get { return orderList; }
set
{
orderList = value;
PropertyChanged(this, new PropertyChangedEventArgs("OrderList"));
}
}
//
private void FindDeliveryOrders(IEnumerable<Order> sortList)
{
foreach (var order in sortList)
{
if (order.Delivery.Equals("Yes"))
{
//deliveryOrders.Add(order);
this.ListViewText.Items.Add(new Order { Firstname = order.Firstname, Lastname = order.Lastname, Ordername = order.Ordername, Deliverytime = order.Deliverytime, Phone = order.Phone, Address = order.Address, Email = order.Email });
}
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
And Of course these are incomplete codes because I don't know how to proceed next.
My goal is just to populate the ListView and it will automatically update itself if orderList changes. But right now I couldn't even know whether the ViewModel is working or not, any thoughts and code demo would be very grateful.
A UserControl should never have a "private" view model, as you assign it to the DataContext in the UserControl's XAML. It should instead expose dependency properties that could be bound to properties of an externally provided view model object.
Declare an ItemsSource property like this:
public partial class UserControl1 : UserControl
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
nameof(ItemsSource), typeof(IEnumerable), typeof(UserControl1));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public UserControl1()
{
InitializeComponent();
}
}
And bind the ListView like this:
<UserControl ...>
...
<ListView ItemsSource="{Binding ItemsSource,
RelativeSource={RelativeSource AncestorType=UserControl}}">
...
</ListView>
...
</UserControl>
When you use the UserControl, bind the property to a view model property:
<TabItem Header="UC1">
<local:UserControl1 ItemsSource="{Binding OrderList}"/>
</TabItem>
The last XAML snippet assumes that the object in the UserControl's DataContext has a OrderList property. This would automatically happen when the TabControl is bound to a collection of view model objects with that property.
Alternatively, let the elements in the UserControl's XAML directly bind to the properties of the object in the inherited DataContext.
<UserControl ...>
...
<ListView ItemsSource="{Binding OrderList}">
...
</ListView>
...
</UserControl>
Your control would not have to expose additional bindable properties, but it would only work with DataContext objects that actually provide the expected source properties.

C# WPF Loop & Delete Checked ListView Items

As someone who has recently switched from WinForms to WPF, I'm still struggling and going nuts trying to figure out a way to loop through and delete checked ListView items.
This method gives me error: "ListView does not contain a definition for CheckedItems..."
if (lvFilesList != null)
{
foreach (ListViewItem lvItem in lvFilesList.CheckedItems)
{
lvItem.Checked = False;
}
}
My XAML Code:
<ListView Height="400" Width="400"
Name="lvFilesList"
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox Name="chk" IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></CheckBox>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
<GridViewColumn Header="File" DisplayMemberBinding="{Binding File}"/>
<GridViewColumn Header="Author" DisplayMemberBinding="{Binding Author}"/>
</GridView.Columns>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Text="Group"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
It looks odd that the ItemsSource of your ListView is directly bound to the current DataContext by
ItemsSource="{Binding}"
The DataContext would usually hold a view model object with a collection-type property like
public class Item
{
public bool IsChecked { get; set; }
// other properties like ID, File, Author
}
public class ViewModel
{
public ObservableCollection<Item> Items { get; }
= new ObservableCollection<Item>();
}
and the Binding would be
ItemsSource="{Binding Items}"
Then the view model could have a method that deletes all checked items, like
public void DeleteCheckedItems()
{
var checkedItems = Items.Where(item => item.IsChecked).ToList();
checkedItems.ForEach(item => Items.Remove(item));
}
Note that you usually assign an instance of the view model class to the DataContext of your main view, e.g. in the MainWindow constructor:
private readonly ViewModel viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = viewModel;
viewModel.Items.Add(new Item { ID = 1, ... });
viewModel.Items.Add(new Item { ID = 2, ... });
}

How to access ProgressBar in ListView in C# WPF without Name

I'm working on a project in C# with WPF. I have a ListView that is populated by binds to an object, which _name is a field of. I want to be able to update a specific ProgressBar by knowing which _name I want to update. So if the current _name is "Task A", I want to update the ProgressBar in the same row as "Task A". However, since I can't name the progress bars (I got an error message when trying to bind data to the Name), I haven't been able to figure out how to access the ProgressBar from the code. I've tried using Tags, but I haven't been able to figure out how to access a control with a certain tag. Any help would be greatly appreciated. Shown below is some of the XAML of my project.
<ListView.View>
<GridView>
<GridViewColumn Width="30">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox Tag="{Binding _name}" IsChecked="True"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="200" DisplayMemberBinding="{Binding _name}">Task Name</GridViewColumn>
<GridViewColumn Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ProgressBar Width="145" Height="15" Maximum="100" Value="{Binding _progress}" Tag="{Binding _name}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
You don't access the ProgessBar element. Instead you bind its Value property to a property of the source object that you update.
_progress and _name (you should rename these by the way) should be public properties of your data type, i.e. the type T of the IEnumerable<T> that you use as the ItemsSource for the ListView.
This class should implement the INotifyPropertyChanged :
public class DataObject : INotifyPropertyChanged
{
private double _progress;
public double Progress
{
get { return _progress; }
set { _progress = value; NotifyPropertyChanged("Progress"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
<DataTemplate>
<ProgressBar Width="145" Height="15" Maximum="100" Value="{Binding Progress}" Tag="{Binding _name}"/>
</DataTemplate>
You then simply change the Progress property of the specific item in the ItemsSource collection of the ListView, .e.g.:
var col = listView.ItemsSource as List<DataObject>;
var item = col.FirstOrDefault(x => x.Name == "some name...");
if(item != null)
item.Value = 100.0;

Create columns dynamically in a GridView in WPF MVVM

My MVVM WPF application currently has a GridView that binds to a ViewModel property and has the columns defined in the XAML:
<ListView Grid.Row="0" ItemsSource="{Binding GroupedOrders}">
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Item2, Mode=OneWay}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Date" DisplayMemberBinding="{Binding Item1.Date, StringFormat={}{0:dd/MM/yyyy}}" />
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Item1.Name}" />
<!-- lots more -->
</GridView>
</ListView.View>
</ListView>
GroupedOrders is an ObservableCollection of a Tuple of two different item types: an "Order" object and a Boolean that controls whether or not, for this particular view, it is "selected".
However, that "selected" property hasn't been modelled well. It's come to light that each Order needs multiple "selected" properties, depending on the number of dates in the order, which can be between one and five.
To model this in the UI, I need to replace that single Checkbox GridViewColumn with a dynamic construct that creates a similar Checkbox GridviewColumn for each date in the order. So GroupedOrders becomes a Tuple <Order, List<bool>> instead, and there will need to be one column for each bool in the List.
At any given instance, the size of that list will be the same for all the Orders in the grid. But if the user loads new data into the grid, the size of the list will change.
However, I cannot see how to do this in MVVM. Existing solutions seem to be for code-behind where the GridView can be grabbed as an object and manipulated on the fly. The only MVVM solution I've seen to this is to use a Converter to building the entire GridView on the fly, which would seem to be massive overkill for this situation, where there are a lot of columns in the GridView but only a small number need to be dynamic.
Is there another way?
Not sure this is what you expect, but this is what I can think of.
View
<ListView ItemsSource="{Binding Orders}">
<ListView.View>
<GridView>
<GridViewColumn Header="State">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding States}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<Style TargetType="GridViewColumnHeader">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Value}" Content="{Binding Text}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="OrderNo" DisplayMemberBinding="{Binding OrderNo}" />
</GridView>
</ListView.View>
</ListView>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Orders.Add(new Order { OrderNo = "Order001" });
Orders.Add(new Order { OrderNo = "Order002" });
Orders.Add(new Order { OrderNo = "Order003" });
}
private readonly ObservableCollection<Order> _orders = new ObservableCollection<Order>();
public ObservableCollection<Order> Orders
{
get { return _orders; }
}
}
public class Order
{
public Order()
{
States.Add(new State { Text = "Is Paid", Value = false });
States.Add(new State { Text = "Is Delivered", Value = false });
}
public string OrderNo { get; set; }
private readonly ObservableCollection<State> _states = new ObservableCollection<State>();
public ObservableCollection<State> States
{
get { return _states; }
}
}
public class State
{
public string Text { get; set; }
public bool Value { get; set; }
}
Result

unable to set selected value of "comboBox" in "ListView"

I am unable to set selected value of "comboBox" in "ListView".
Here is XAML code.
Propertyname: LISTTOPICS
<ListView x:Name="gridTopics"
ItemsSource="{Binding Path=TOPICSINFO}" Width="310">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Associated Topics" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding LISTTOPICS}"
SelectedValue="{Binding SelectedTopic.SELECTEDTOPIC}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
And C# code is
public class SelectedTopic : ObservableObject
{
private static string selectedTopic;
public static string SELECTEDTOPIC
{
get { return selectedTopic; }
set { selectedTopic = value; }
}
}
You need to call RaisePropertyChanged in your setter.
You have two options (it's difficult to say precisely, because DataContext isn't clear from your question):
change binding expression to something like SelectedValue="{Binding SELECTEDTOPIC}" .
properly binds to static property: Binding to static class property.

Categories