I've been trying to Bind the two ListViews to ViewModel. Both lists are loading the items properly. But to my surprise I've encountered a little problem.
The first ListView's SelectedItem binds correctly but the second one doesn't bind! As shown in the image below. What could be the reason?
XAML:
<Window x:Class="Test.Dialogs.BeamElevationsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Test.Dialogs.Converters"
Title="Select Beam Elevation" Height="350" Width="460"
Style="{StaticResource DialogStyle}"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<converters:ElevationValueConverter x:Key="ElevationValueConverter"/>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<GroupBox>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="175"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="215"/>
</Grid.ColumnDefinitions>
<GroupBox Header="Typs">
<ListView ItemsSource="{Binding TypIds}"
SelectedItem="{Binding CurrentTypId}">
<ListView.View>
<GridView AllowsColumnReorder="False"
ColumnHeaderContainerStyle="{StaticResource DialogsGridViewColumnHeaderStyle}" >
<GridViewColumn Header="Typ."/>
</GridView>
</ListView.View>
</ListView>
</GroupBox>
<GroupBox Grid.Row="0" Grid.Column="2" Header="Elevations">
<ListView ItemsSource="{Binding Elevations}"
SelectedItem="{Binding CurrentBeamElevation}">
<ListView.View>
<GridView AllowsColumnReorder="False"
ColumnHeaderContainerStyle="{StaticResource DialogsGridViewColumnHeaderStyle}" >
<GridViewColumn Header="Typ." />
</GridView>
</ListView.View>
</ListView>
</GroupBox>
</Grid>
</GroupBox>
<Grid Grid.Row="1">
<Button Content="OK"/>
</Grid>
</Grid>
</Window>
Code-Behind:
public partial class BeamElevationsWindow
{
private BeamElevationsViewModel ViewModel { get; set; }
public BeamElevationsWindow()
{
InitializeComponent();
ViewModel = new BeamElevationsViewModel();
DataContext = ViewModel;
}
}
ViewModel:
namespace Test.Dialogs.ViewModels
{
public class BeamElevationsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public BeamElevationsViewModel()
{
var frames = Building.Frames
.GroupBy(f => f.TypId)
.Select(group => group.First())
.OrderBy(f => f.TypId)
.ToList();
typIds = new List<int>();
foreach (var frame in frames)
{
typIds.Add(frame.TypId);
}
TypIds = typIds;
CurrentTypId = Building.CurrentFrame.TypId;
GetElevations(CurrentTypId);
CurrentBeamElevation = Building.CurrentBeamElevation;
}
public void GetElevations(int typId)
{
var frames = Building.Frames
.Where(f => f.TypId == typId)
.OrderByDescending(f => f.Elevation)
.ToList();
elevations = new List<Elevation>();
foreach (var fr in frames)
{
foreach (var elevation in Building.Elevations)
{
if (Math.Abs(fr.Elevation - elevation.El) < Arithmetics.Tolerance)
{
elevations.Add(elevation);
break;
}
}
}
Elevations = elevations;
}
private List<int> typIds;
public List<int> TypIds
{
get { return typIds; }
private set
{
typIds = value;
RaisePropertyChanged("TypIds");
}
}
private int currentTypId;
public int CurrentTypId
{
get { return currentTypId; }
private set
{
currentTypId = value;
RaisePropertyChanged("CurrentTypId");
}
}
private List<Elevation> elevations;
public List<Elevation> Elevations
{
get { return elevations; }
private set
{
elevations = value;
RaisePropertyChanged("Elevations");
}
}
private Elevation currentBeamElevation;
public Elevation CurrentBeamElevation
{
get { return currentBeamElevation; }
private set
{
currentBeamElevation = value;
RaisePropertyChanged("CurrentBeamElevation");
}
}
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The binding is actually working fine :)
However, the default comparer for object does a reference comparison. That means that when its trying to find the existing object in the list, it doesn't pick any of them because they aren't the same instance (per your comment).
The solution is to override Object.Equals (and when you override that, you should also override Object.GetHashCode). It should test equality based on some unique property of the object, so you don't get false positives.
Related
Problem
I want to refresh my wpf view when a change is made in a List of objects in my application, but it wont register the INotifyChanged method when I change a value.
What I've tried
I went to multiple different stackoverflow pages with sort of the same problem but I don't get it working right. It wont register a change in a object in the list.
my code
below is the code for the MainWindow of the WPF application in wher with the last button click I change the value of XLocation in an object out of a list.
public partial class MainWindow : Window
{
private string filePathArtist { get; set; }
private string filePathGrid { get; set; }
public Game.Game Game { get; set; }
public MainWindow()
{
InitializeComponent();
filePathGrid = String.Empty;
filePathArtist = String.Empty;
}
private void BtnOpen_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
bool? res = openFileDialog.ShowDialog();
if (res == true)
{
string filepathgrid = openFileDialog.FileName;
filePathGrid = filepathgrid;
GridTextBox.Text = filepathgrid;
}
}
private void PickArtistBtn_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
bool? res = openFileDialog.ShowDialog();
if (res == true)
{
string filepathartist = openFileDialog.FileName;
filePathArtist = filepathartist;
ArtistTextBox.Text = filepathartist;
}
}
private void CreateGridBtn_Click(object sender, RoutedEventArgs e)
{
Game = new Game.Game(filePathGrid, filePathArtist);
this.DataContext = Game;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Game.Artists[0].XLocation = 30;
}
}
Next code is the Game class where is implemented a INotfyPropertyChanged on the list of Artists.
public class Game : INotifyPropertyChanged
{
public List<Artist> _artists;
public List<Artist> Artists
{
get
{
return _artists;
}
set
{
_artists = value;
OnPropertyChanged("Artists");
}
}
public List<ITile> Tiles { get; set; }
public Game()
{
}
public Game(string graphPath, string artistPath)
{
IDataParser graphParser = DataFactory.DataFactory.Instance.CreateParser(graphPath);
IDataParser artistParser = DataFactory.DataFactory.Instance.CreateParser(artistPath);
Tiles = graphParser.ParseGridData(graphPath);
Artists = artistParser.ParseArtistData(artistPath);
Test = "new Game";
}
public string Test { get; set; } = "t";
public event PropertyChangedEventHandler? PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Ive also added the INotifyPropertyChanged in the Artists class
public class Artist : INotifyPropertyChanged
{
private float _xLocation;
private float _yLocation;
private int _xVelocity;
private int _yVelocity;
public float XLocation
{
get => _xLocation;
set
{
_xLocation = value;
OnPropertyChanged("XLocation");
}
}
public float ConvertedXLoc
{
get => XLocation * (float)3.75;
set { }
}
public float YLocation
{
get => _yLocation;
set
{
_yLocation = value;
OnPropertyChanged("YLocation");
}
}
public float ConvertedYLoc
{
get => YLocation * (float)3.75;
set { }
}
public int XVelocity
{
get => _xVelocity;
set
{
_xVelocity = value;
}
}
public int YVelocity
{
get => _yVelocity;
set
{
_yVelocity = value;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then here is the Xaml code where I bind the objects to the wpf UI.
<Window x:Class="BroadwayBoogie.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:game="clr-namespace:BroadwayBoogie.Game"
mc:Ignorable="d"
Title="MainWindow" Height="900" Width="900"
>
<Window.DataContext>
<game:Game/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="201*"/>
<ColumnDefinition Width="199*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="21*"/>
<RowDefinition Height="401*"/>
<RowDefinition Height="20*"/>
</Grid.RowDefinitions>
<Button x:Name="BtnOpen" Content="Pick Grid" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Click="BtnOpen_Click"/>
<TextBox x:Name="GridTextBox" HorizontalAlignment="Center" TextWrapping="NoWrap" VerticalAlignment="Center" Width="266" />
<Button x:Name="PickArtistBtn" Content="Pick Artist" HorizontalAlignment="Left" Margin="356,0,0,0" VerticalAlignment="Center" Click="PickArtistBtn_Click" RenderTransformOrigin="-0.135,0.647"/>
<TextBox x:Name="ArtistTextBox" HorizontalAlignment="Left" Margin="30,14,0,0" TextWrapping="NoWrap" VerticalAlignment="Top" Width="231" Grid.Column="1"/>
<Button x:Name="CreateGridBtn" Grid.Column="1" Content="Create Grid" HorizontalAlignment="Left" Margin="311,14,0,0" VerticalAlignment="Top" Click="CreateGridBtn_Click"/>
<Canvas Width="800" Height="800" Grid.ColumnSpan="2" Margin="49,15,51,27" Grid.Row="1" Background="DarkSeaGreen" Grid.RowSpan="2">
<ItemsControl Name="tilesItemsControl" ItemsSource="{Binding Tiles}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<Rectangle
Width="15"
Height="15"
Fill="{Binding Color}"
Canvas.Left ="{Binding ConvertedXLoc}"
Canvas.Top="{Binding ConvertedYLoc}" />
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Name="ArtistItemsControl" ItemsSource="{Binding Artists}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<Rectangle
Width="3.75"
Height="3.75"
Fill="Black"
Canvas.Left ="{Binding ConvertedXLoc}"
Canvas.Top="{Binding ConvertedYLoc}" />
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
<Grid/>
<Button Content="Button" HorizontalAlignment="Left" Margin="4,0,0,0" Grid.Row="2" VerticalAlignment="Center" Click="Button_Click"/>
</Grid>
So with the press of the button I added for testing purposes. It changes a value in the List and then the PropertyChanged method should detect that but it doesn't detect it it just skips it.
Question
So my basic question is, how do I detect the change of a property from objects out of the List of Artists.
OnPropertyChanged will only be executed when the property itself is changed. A new item in a list is not a property change. That's the reason why no updates happens.
Instead a List, try an ObservableCollection. An ObservableCollection implements an additional INotifyCollectionChanged which makes the UI able to react on changing items in the list.
Prerequisites: .NET 4.5.1
I have three TreeView controls that display three filtered variants of single collection instance. When I try to apply a filter on Items collection of one of controls this filter propagates to other controls automatically which prevents me to use different filters on different controls.
Is there any way to achieve the same result without having to maintain three instances of collections at once?
An example that shows the problem follows below. First two ListViews are bound to the same collection instance directly. Third one is bound to that instance through CompositeCollection. And the fourth is bound to independent collection. When I press "Set Filter" button ItemsControl.Items.Filter property if first ListView is set to IsAllowedItem method of WTest window. After this second istView.Items.Filter property somehow points to the same method while third and fourth ListView returns null. Another effect is that though third ListView shows null filter its collection is still filtered as you can see if you run the example. This very strange effect arises from the behavior of ItemCollection class that when based on ItemsSource property of owner element acquires underlying CollectionView from some application-wide storage via CollectionViewSource.GetDefaultCollectionView method. I don't know the reason of this implementation but suspect suspect that it's performance.
Test window WTest.xaml:
<Window x:Class="Local.WTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:c="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:local="clr-namespace:Local"
Name="_WTest" Title="WTest" Height="300" Width="600">
<Window.Resources>
<c:ArrayList x:Key="MyArray">
<s:String>Letter A</s:String>
<s:String>Letter B</s:String>
<s:String>Letter C</s:String>
</c:ArrayList>
<CompositeCollection x:Key="MyCollection" >
<CollectionContainer Collection="{StaticResource ResourceKey=MyArray}"/>
</CompositeCollection>
<c:ArrayList x:Key="AnotherArray">
<s:String>Letter A</s:String>
<s:String>Letter B</s:String>
<s:String>Letter C</s:String>
</c:ArrayList>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Name="FilterLabel1"/>
<TextBlock Grid.Row="0" Grid.Column="1" Name="FilterLabel2"/>
<TextBlock Grid.Row="0" Grid.Column="2" Name="FilterLabel3"/>
<TextBlock Grid.Row="0" Grid.Column="3" Name="FilterLabel4"/>
<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
<ListView Grid.Row="1" Grid.Column="1" Name="View2" ItemsSource="{StaticResource ResourceKey=MyArray}"/>
<ListView Grid.Row="1" Grid.Column="2" Name="View3" ItemsSource="{StaticResource ResourceKey=MyCollection}"/>
<ListView Grid.Row="1" Grid.Column="3" Name="View4" ItemsSource="{StaticResource ResourceKey=AnotherArray}"/>
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Content="Set Filter" Click="OnSetFilterButtonClick"/>
</Grid>
</Window>
Code behind WTest.xaml.cs
namespace Local
{
using System.Windows;
public partial class WTest : Window
{
public WTest()
{
InitializeComponent();
UpdateFilterLabels();
}
private bool IsAllowedItem(object item)
{
return "Letter A" == (string)item;
}
private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
{
View1.Items.Filter = IsAllowedItem;
UpdateFilterLabels();
}
private void UpdateFilterLabels()
{
FilterLabel1.Text = (null == View1.Items.Filter) ? "No Filter" : View1.Items.Filter.Method.Name;
FilterLabel2.Text = (null == View2.Items.Filter) ? "No Filter" : View2.Items.Filter.Method.Name;
FilterLabel3.Text = (null == View3.Items.Filter) ? "No Filter" : View3.Items.Filter.Method.Name;
FilterLabel4.Text = (null == View4.Items.Filter) ? "No Filter" : View4.Items.Filter.Method.Name;
}
}
}
And result after "Set Filter" button is clicked:
Example: result of clicking "Set Filter" button
Create CollectionViewSource as a Resource.
<CollectionViewSource x:Key="CVSKey" Source="{DynamicResource MyArray}"/>
Use this CollectionViewSource as your ItemsSource . Replace your View1 as :
<!--<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{DynamicResource ResourceKey=MyArray}"/>-->
<ListView Grid.Row="1" Grid.Column="0" Name="View1" ItemsSource="{Binding Source={StaticResource ResourceKey=CVSKey}}"/>
Thats it, now everything will work as you want it to.
Additionally, now you can apply filtering to this CollectionViewSource instead of View1 :
((CollectionViewSource)this.Resources["CVSKey"]).Filter += List_Filter;
void List_Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item.ToString() == "Letter A") ? true : false;
}
Create separate CollectionViewSource for separate ListBoxes to create separate views from same underlying collection.
Search google for CollectionViewSource.
Change the OnSetFilterButtonClick Method as below
private void OnSetFilterButtonClick(object sender, RoutedEventArgs e)
{
//Create a new listview by the ItemsSource,Apply Filter to the new listview
ListCollectionView listView = new ListCollectionView(View1.ItemsSource as IList);
listView.Filter = IsAllowedItem;
View1.ItemsSource = listView;
UpdateFilterLabels();
}
I found a simple solution that does not require creating a CollectionViewSource resource in XAML or a ListCollectionView in code for every collection that needs its own filter.
My solution is to use an ValueConverter that converts the ItemsSource source to a CollectionViewSource.View
ItemsSourceConverter:
[ValueConversion(typeof(IEnumerable), typeof(IEnumerable))]
public class ItemsSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is IEnumerable itemsSource && itemsSource != null)
{
return new CollectionViewSource() { Source = itemsSource }.View;
}
else
{
throw new Exception($"Value must be an {nameof(IEnumerable)}");
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return DependencyProperty.UnsetValue;
}
}
XAML:
<Window ...>
<Window.Resources>
<local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
</Window.Resources>
...
<ItemsControl Name="View1",
ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
<ItemsControl Name="View2",
ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
<ItemsControl Name="View3",
ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}" />
</Window>
Code behind:
public partial class MainWindow : Window
{
ObservableCollection<DataClass> Collection1 { get; private set; }
ObservableCollection<DataClass> Collection2 { get; private set; }
ObservableCollection<DataClass> Collection3 { get; private set; }
public MainWindow()
{
InitializeComponent();
}
...
private void SetFilters()
{
View1.Filter = (item) =>
{
// Filter logic
};
View2.Filter = (item) =>
{
// Filter logic
};
View2.Filter = (item) =>
{
// Filter logic
};
}
...
}
MVVM ItemsControl with Filter binding
If we want to use the above solution with MVVM, we can create an attached property to bind the ItemsControl.Filter to a filter defined in the ViewModel.
Filter attached property:
public static class CollectionViewExtensions
{
public static readonly DependencyProperty FilterProperty = DependencyProperty.RegisterAttached(
"Filter",
typeof(Predicate<object>),
typeof(CollectionViewExtensions),
new PropertyMetadata(default(Predicate<object>), OnFilterChanged));
public static void SetFilter(ItemsControl element, Predicate<object> value)
{
element.SetValue(FilterProperty, value);
}
public static Predicate<object> GetFilter(ItemsControl element)
{
return (Predicate<object>)element.GetValue(FilterProperty);
}
private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ItemsControl itemsControl && itemsControl.Items.CanFilter)
{
if (e.OldValue is Predicate<object> oldPredicate)
{
itemsControl.Items.Filter -= oldPredicate;
}
if (e.NewValue is Predicate<object> newPredicate)
{
itemsControl.Items.Filter += newPredicate;
}
}
}
}
Source: https://stackoverflow.com/a/39438710/10927863
XAML:
<Window ...>
<Window.Resources>
<local:ItemsSourceConverter x:Key="ItemsSourceConverter"/>
</Window.Resources>
...
<ItemsControl ItemsSource="{Binding Collection1, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter1}"/>
<ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter2}"/>
<ItemsControl ItemsSource="{Binding Collection3, Converter={StaticResource ItemsSourceConverter}, Mode=OneWay}"
local:CollectionViewExtensions.Filter="{Binding Filter3}"/>
</Window>
ViewModel:
public class ViewModel
{
ObservableCollection<DataClass> Collection1 { get; private set; }
ObservableCollection<DataClass> Collection2 { get; private set; }
ObservableCollection<DataClass> Collection3 { get; private set; }
public Predicate<object> Filter1 { get; private set; }
public Predicate<object> Filter2 { get; private set; }
public Predicate<object> Filter3 { get; private set; }
...
private void SetFilters()
{
Filter1 = (item) =>
{
// Filter logic
};
Filter2 = (item) =>
{
// Filter logic
};
Filter3 = (item) =>
{
// Filter logic
};
}
...
}
I would like to get content from my combobox. I already tried some ways to do that, but It doesn't work correctly.
This is example of my combobox:
<ComboBox x:Name="cmbSomething" Grid.Column="1" Grid.Row="5" HorizontalAlignment="Center" Margin="0 100 0 0" PlaceholderText="NothingToShow">
<ComboBoxItem>First item</ComboBoxItem>
<ComboBoxItem>Second item</ComboBoxItem>
</ComboBox>
After I click the button, I want to display combobox selected item value.
string selectedcmb= cmbSomething.Items[cmbSomething.SelectedIndex].ToString();
await new Windows.UI.Popups.MessageDialog(selectedcmb, "Result").ShowAsync();
Why this code does not work?
My result instead of showing combobox content, it shows this text:
Windows.UI.Xaml.Controls.ComboBoxItem
You need the Content property of ComboBoxItem. So this should be what you want:
var comboBoxItem = cmbSomething.Items[cmbSomething.SelectedIndex] as ComboBoxItem;
if (comboBoxItem != null)
{
string selectedcmb = comboBoxItem.Content.ToString();
}
I have expanded on my suggestion regarding using models instead of direct UI code-behind access. These are the required parts:
BaseViewModel.cs
I use this in a lot of the view models in my work project. You could technically implement it directly in a view model, but I like it being centralized for re-use.
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Hashtable values = new Hashtable();
protected void SetValue(string name, object value)
{
this.values[name] = value;
OnPropertyChanged(name);
}
protected object GetValue(string name)
{
return this.values[name];
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
ComboViewModel.cs
This what you'll bind to make it easy to get values. I called it ComboViewModel because I'm only dealing with your ComboBox. You'll want a much bigger view model with a better name to handle all of your data binding.
public class ComboViewModel : BaseViewModel
{
public ComboViewModel()
{
Index = -1;
Value = string.Empty;
Items = null;
}
public int Index
{
get { return (int)GetValue("Index"); }
set { SetValue("Index", value); }
}
public string Value
{
get { return (string)GetValue("Value"); }
set { SetValue("Value", value); }
}
public List<string> Items
{
get { return (List<string>)GetValue("Items"); }
set { SetValue("Items",value); }
}
}
Window1.xaml
This is just something I made up to demonstrate/test it. Notice the various bindings.
<Window x:Class="SO37147147.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="cmbSomething" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" HorizontalAlignment="Center" MinWidth="80"
ItemsSource="{Binding Path=Items}" SelectedIndex="{Binding Path=Index}" SelectedValue="{Binding Path=Value}"></ComboBox>
<TextBox x:Name="selectedItem" MinWidth="80" Grid.Row="2" Grid.Column="0" Text="{Binding Path=Value}" />
<Button x:Name="displaySelected" MinWidth="40" Grid.Row="2" Grid.Column="1" Content="Display" Click="displaySelected_Click" />
</Grid>
</Window>
Window1.xaml.cs
Here's the code-behind. Not much to it! Everything is accessed through the dataContext instance. There's no need to know control names, etc.
public partial class Window1 : Window
{
ComboViewModel dataContext = new ComboViewModel();
public Window1()
{
InitializeComponent();
dataContext.Items=new List<string>(new string[]{"First Item","Second Item"});
this.DataContext = dataContext;
}
private void displaySelected_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(String.Format("Selected item:\n\nIndex: {0}\nValue: {1}", dataContext.Index, dataContext.Value));
}
}
You can add business logic for populating models from a database, saving changes to a database, etc. When you alter the properties of the view model, the UI will automatically be updated.
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.
I'm trying to learn Prism MVVM, and i'm making a window with 2 fields and a button, that gets enabled when this two fields aren't empty.
The problem is that i can't find a way to make the method ObservesProperty() work on an object (Pessoa in that case). The CanExecuteAtualizar() method only gets called at the app startup, and when i edit the textfields Nome or Sobrenome nothing happens to the button and the method isn't fired...
I tried to work without a model, putting the Nome, Sobrenome and UltimaAtualizacao properties directly in the ViewModel and it works fine, disabling the button according to the return of the method CanExecuteAtualizar, but i wanted to use it with a model instead. Is there a way to do this?
ViewAViewModel.cs
public class ViewAViewModel : BindableBase
{
private Pessoa _pessoa;
public Pessoa Pessoa
{
get { return _pessoa; }
set { SetProperty(ref _pessoa, value); }
}
public ICommand CommandAtualizar { get; set; }
public ViewAViewModel()
{
Pessoa = new Pessoa();
Pessoa.Nome = "Gabriel";
CommandAtualizar = new DelegateCommand(ExecuteAtualizar, CanExecuteAtualizar).ObservesProperty(() => Pessoa.Nome).ObservesProperty(() => Pessoa.Sobrenome);
}
public bool CanExecuteAtualizar()
{
return !string.IsNullOrWhiteSpace(Pessoa.Nome) && !string.IsNullOrWhiteSpace(Pessoa.Sobrenome);
}
public void ExecuteAtualizar()
{
Pessoa.UltimaAtualizacao = DateTime.Now;
}
}
Pessoa.cs
public class Pessoa : BindableBase
{
private string _nome;
public string Nome
{
get { return _nome; }
set { SetProperty(ref _nome, value); }
}
private string _sobrenome;
public string Sobrenome
{
get { return _sobrenome; }
set { SetProperty(ref _sobrenome, value); }
}
private DateTime? _ultimaAtualizacao;
public DateTime? UltimaAtualizacao
{
get { return _ultimaAtualizacao; }
set { SetProperty(ref _ultimaAtualizacao, value); }
}
}
ViewA.xaml
<UserControl x:Class="PrismDemo.Views.ViewA"
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:PrismDemo.Views"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="500">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Content="Nome:" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" Margin="3" TabIndex="0" Text="{Binding Pessoa.Nome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="Sobrenome:" Grid.Column="0" Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" TabIndex="1" Text="{Binding Pessoa.Sobrenome, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="Última atualização:" Grid.Column="0" Grid.Row="2" HorizontalAlignment="Left" VerticalAlignment="Center" />
<Label Grid.Column="1" Grid.Row="2" Margin="3" HorizontalAlignment="Left" Content="{Binding Pessoa.UltimaAtualizacao, Mode=TwoWay}" />
<Button Content="Atualizar" Grid.Column="1" Grid.Row="3" Width="70" Margin="2,2,3,2" HorizontalAlignment="Right" Command="{Binding CommandAtualizar}" />
</Grid>
</UserControl>
DelegateCommand.ObservesPropery doesn't support complex object properties. It only supports properties that exist on the ViewModel in the command is defined. This is because the lifecycle of complex objects are unknown, and a memory leak would be created if many instances of the object was created. My recommendation would be to define you property like this:
private Pessoa _pessoa;
public Pessoa Pessoa
{
get { return _pessoa; }
set
{
if (_pessoa != null)
_pessoa.PropertyChanged -= PropertyChanged;
SetProperty(ref _pessoa, value);
if (_pessoa != null)
_pessoa.PropertyChanged += PropertyChanged;
}
}
Then in the PropertyChanged method, call DelegateCommand.RaiseCanExecuteChanged
EDIT:
Complex property support is now available in Prism for Xamarin.Forms 7.0.
It is true that DelegateCommand.ObservesPropery does not support complex objects, but its the way Commands are meant to be used with Prism. Manually calling PropertyChanged is an ugly hack in my opinion and should be avoided. Also it bloates the code again which Prism tries to reduce.
Moving all properties of the complex type into the ViewModel on the other hand would reduce the readability of the ViewModel. The very reason you create complex types in such scenarios is to avoid having too many single properties there in the first place.
But instead you could move the Command definition inside the complex type. Then you can set ObservesProperty for all simple properties in the constructor of the complex type and everything works as expected.
Model:
using Prism.Commands;
using Prism.Mvvm;
using static System.String;
public class LoginData : BindableBase
{
public LoginData()
{
DbAddr = DbName = DbUser = DbPw = "";
TestDbCommand = new DelegateCommand(TestDbConnection, CanTestDbConnection)
.ObservesProperty(() => DbAddr)
.ObservesProperty(() => DbName)
.ObservesProperty(() => DbUser)
.ObservesProperty(() => DbPw);
}
public DelegateCommand TestDbCommand { get; set; }
public bool CanTestDbConnection()
{
return !IsNullOrWhiteSpace(DbAddr)
&& !IsNullOrWhiteSpace(DbName)
&& !IsNullOrWhiteSpace(DbUser)
&& !IsNullOrWhiteSpace(DbPw);
}
public void TestDbConnection()
{
var t = new Thread(delegate () {
Status = DatabaseFunctions.TestDbConnection(this);
});
t.Start();
}
private string _dbAddr;
public string DbAddr
{
get => _dbAddr;
set => SetProperty(ref _dbAddr, value);
}
...
}
ViewModel:
public class DatabaseConfigurationViewModel
{
public DatabaseConfigurationViewModel()
{
CurrentLoginData = new LoginData(true);
}
public LoginData CurrentLoginData { get; set; }
}
View:
<UserControl x:Class="TestApp.Views.DatabaseConfiguration"
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:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Vertical">
<Label>IP Adresse oder URL:</Label>
<TextBox Text="{Binding CurrentLoginData.DbAddr, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></TextBox>
...
<Button Command="{Binding CurrentLoginData.TestDbCommand}">Teste Verbindung</Button>
</StackPanel>
</Grid>
In the Prism version that I have (7.0.0.362), you can use ObserveCanExecute, and pass property HasChanges that is updated on every property change of your entity.
TestDbCommand = new DelegateCommand(TestDbConnection).ObservesCanExecute(() => HasChanged);
Pessoa = new Pessoa();
Pessoa.PropertyChanged += Pessoa_PropertyChanged;
Then update HasChanges with a validation method in the constructor, and detach the method in the destructor.
private void Pessoa_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
HasChanged = ValidatePessoa(Pessoa);
}
~YourViewModel()
{
Pessoa.PropertyChanged -= Pessoa_PropertyChanged;
}
bool _hasChanged;
public bool HasChanged { get => _hasChanged; set => SetProperty(ref _hasChanged, value); }