My code looks like this right now with two lines of code for each message. The code works but if I have for example 30 messages that I can each give values to then I will need to have 60 lines of code just to declare everything:
string _msg1;
string _msg2;
public string Msg1 { get => _msg1; set => SetProperty(ref _msg1, value); }
public string Msg2 { get => _msg2; set => SetProperty(ref _msg2, value); }
and in C# I assign to these:
vm.Msg1 = "A";
vm.Msg2 = "B";
and in the XAML I bind my Text to Msg1 and another Text to Msg2
Can someone tell me how / if I can do this with array so that I would assign like this and hopefully so the assignment of the array can just be done in two lines instead of 2 lines for every single message:
vm.Msg[0] = "A";
vm.Msg[1] = "B";
For reference:
public class ObservableObject : INotifyPropertyChanged
{
protected virtual bool SetProperty<T>(
ref T backingStore, T value,
[CallerMemberName]string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
You can create a simple wrapper class with indexing that supports property change notification.
For example:
public class Messages : ObservableObject
{
readonly IDictionary<int, string> _messages = new Dictionary<int, string>();
[IndexerName("Item")] //not exactly needed as this is the default
public string this[int index]
{
get
{
if (_messages.ContainsKey(index))
return _messages[index];
//Uncomment this if you want exceptions for bad indexes
//#if DEBUG
// throw new IndexOutOfRangeException();
//#else
return null; //RELEASE: don't throw exception
//#endif
}
set
{
_messages[index] = value;
OnPropertyChanged("Item[" + index + "]");
}
}
}
And, create a property in view model as:
private Messages _msg;
public Messages Msg
{
get { return _msg ?? (_msg = new Messages()); }
set { SetProperty(ref _msg, value); }
}
Now you can set or update values as:
vm.Msg[0] = "A";
vm.Msg[1] = "B";
Bindings in XAML will be same as:
<Label Text="{Binding Msg[0]}" />
<Label Text="{Binding Msg[1]}" />
Sample usage code
XAML
<StackLayout Margin="20">
<Label Text="{Binding Msg[0]}" />
<Label Text="{Binding Msg[1]}" />
<Label Text="{Binding Msg[2]}" />
<Label Text="{Binding Msg[3]}" />
<Label Text="{Binding Msg[4]}" />
<Button Text="Trigger update" Command="{Binding UpdateMessage}" />
</StackLayout>
Code-behind, view-model
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
var viewModel = new MainViewModel();
viewModel.Msg[0] = "Original message 1";
viewModel.Msg[1] = "Original message 2";
viewModel.Msg[2] = "Original message 3";
viewModel.Msg[3] = "Original message 4";
viewModel.Msg[4] = "Original message 5";
BindingContext = viewModel;
}
}
public class MainViewModel : ObservableObject
{
private Messages _msg;
public Messages Msg
{
get { return _msg ?? (_msg = new Messages()); }
set { SetProperty(ref _msg, value); }
}
public ICommand UpdateMessage => new Command(() =>
{
Msg[2] = "New message 3";
Msg[0] = "New message 1";
});
}
Arrays will not raise property changed event. You'll need to use an ObservableCollection that can raise an event when the collection has changed. However, this doesn't raise an event when the object inside the collection has changed it's value. You'll need to wrap your object, in this case a string, into a type that can raise property changed events.
Something like the following would work:
public class BindableValue<T> : INotifyPropertyChanged
{
private T _value;
public T Value
{ get => _value; set => SetProperty(ref _value, value); }
// INotifyPropertyChanged and SetProperty implementation goes here
}
private ObservableCollection<BindableValue<string>> _msg;
public ObservableCollection<BindableValue<string>> Msg
{ get => _msg; set => SetProperty(ref _msg1, value); }
you would be binding to Msg[0].Value, Msg[1].Value etc.,
Not entirely sure that I got the question, but as I understood the simplest way is this:
The Viewmodel:
Just bind to an ObservableCollection of strings, because it already implements INotifyCollectionChanged and INotifyPropertyChanged.
RelayCommand is just an implementation of ICommand and I'm assuming you have heard of them since you are doing WPF MVVM.
using System.Collections.ObjectModel;
namespace WpfApp1
{
public class MainWindowViewmodel
{
public ObservableCollection<string> Messages { get; set; }
public MainWindowViewmodel()
{
Messages = new ObservableCollection<string>();
Messages.Add("My message!");
ChangeMessageCommand = new RelayCommand(ChangeMessageExcecute);
}
public RelayCommand ChangeMessageCommand { get; set; }
private void ChangeMessageExcecute() => Messages[0] = "NEW message!";
}
}
The View:
In the view you can just bind your Textblocks to the Elements of the ObservableCollection. When you press the button, the Command gets called and changes the message in the window.
<Window x:Class="WpfApp1.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"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel>
<TextBlock Text="{Binding Messages[0]}" HorizontalAlignment="Center"/>
<Button Content="Change Message" Command="{Binding ChangeMessageCommand}" Width="200"/>
</StackPanel>
</Grid>
</Window>
Kind regards,
misdirection
I Assume that your given example is running and working as expected (Atleast with 2 items)
View Code.
Assuming you want to show all the 30 messages as a list.
<ListView ItemsSource="{Binding MessagesArray}"/>
Also you should set the DataContext properly, Comment below if you need any help
View Model Code.
We are using an ObservableCollection instead of array. Since pure arrays doesn't support proper binding features.
private ObservableCollection<string> _messagesArray;
public ObservableCollection<string> MessagesArray
{
get { return _messagesArray; }
set { SetProperty(ref _messagesArray, value); }
}
Assigning Values
MessagesArray = new ObservableCollection<string>();
vm.MessagesArray.Add("A");
vm.MessagesArray.Add("B");
In the assignment code MessagesArray = new ObservableCollection<string>(); assigns a new object of ObservableCollection of String
If you are new to ObservableCollection think of this as an wrapper to string[], but not actually true
SetProperty method will tell the XAML View that a new collection is arrived, so the UI will rerender the list.
When you call vm.MessagesArray.Add("B"); internal logics inside the method Add will tell the XAML View a new item is added to the ObservableCollection so the view can rerender the ListView with the new item.
Update 27 October 2018
You can create your own array using any of the below ways. (Not all)
string[] dataArray = new string[30];
1. this will create an array with 30 null values
string[] dataArray = { "A", "B", "C" }; //Go up to 30 items
2. this will create an array with predefined set of values, you can go up to 30
string[] dataArray = Enumerable.Repeat<string>(String.Empty, 30).ToArray();
3. this will create an array with string which holds empty values, Instead of String.Empty you can put any string value.
Choose any of the above method
I recommend the last method, then you can assign that into a Observable Collection like below.
MessagesArray = new ObservableCollection<string>(dataArray);
Now the trick is
vm.MessagesArray[0] = "A"
vm.MessagesArray[25] = "Z"
View might look like below
<TextBlock Text="{Binding MessagesArray[0]}"/>
<TextBlock Text="{Binding MessagesArray[1]}"/>
What about using reflection?
You can ask for all the public properties of type string with name "Msg*".
For example:
static class Program
{
static void Main(string[] args)
{
var vm = new MessagesViewModel();
PropertyInfo[] myProperties = vm.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.PropertyType == typeof(string) && p.Name.Contains("Msg"))
.ToArray();
foreach (var propertyInfo in myProperties)
{
//You can also access directly using the indexer --> myProperties[0]..
propertyInfo.SetValue(vm, $"This is {propertyInfo.Name} property");
}
Console.WriteLine(vm.Msg1);
Console.WriteLine(vm.Msg2);
}
}
public class MessagesViewModel
{
string _msg1;
string _msg2;
public string Msg1 { get => _msg1; set => _msg1 = value; }
public string Msg2 { get => _msg2; set => _msg2 = value; }
}
If this type of solution fits, you can wrap it with an indexer, sort the array to match the index and the Msg[num].
Related
I have a search bar with the property Text binded to a string property in my ViewModel.
I also have Behaviors within the search bar so that every time the text is changed a search is done within a list of objects using NewTextValue passed to as the query string.
The issue I have is that, I make the ListView invisible until a non-empty string is passed to my Search/Filter command (obviously.. :)). I have tried to enforcing hiding the ListView for a couple scenarios e.g. if all text is removed from the search bar.
When an item is selected from the now visible list view I used that item to populate the Text property of my SearchBar, after which I cannot hide it within code. All attempts have failed and the ListView remains visible. Note: I explicity created a hide button separately and saw it worked so I am wondering if I cannot tie hiding the view with setting the searchbar Text property.
View
<SearchBar Text="{Binding SearchText}">
<SearchBar.Behaviors>
<prismBehaviors:EventToCommandBehavior EventName="TextChanged"
Command="{Binding FilterOccupationsListCommand}"
EventArgsParameterPath="NewTextValue"/>
</SearchBar.Behaviors>
</SearchBar>
<ListView ItemsSource="{Binding FilteredOccupations}" IsVisible="{Binding FilteredOccupationsVisible}" SelectedItem="{Binding Occupation, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Please Note : My ViewModel inherits from BaseViewModel which inherits INotifyPropertyChanged. SetProperty() is what notifies the property. This is quite common with MvvmCross, Prism etc.
ViewModel
public class MyViewModel : BaseViewModel
{
public DelegateCommand<string> FilterOccupationsListCommand { get; }
public MyViewModel()
{
FilterOccupationsListCommand = new DelegateCommand<string>(FilterOccupationsList);
}
private void FilterOccupationsList(string query)
{
if (!string.IsNullOrWhiteSpace(query))
{
FilteredOccupationsVisible = true;
var searchResult = Occupations.Where(x => x.Name.ToLower().Contains(query));
FilteredOccupations = new ObservableCollection<Occupation>(searchResult);
}
else
FilteredOccupationsVisible = false;
}
private Occupation _occupation;
public Occupation Occupation
{
get => _occupation;
set
{
SetProperty(ref _occupation, value);
SearchText = value.Name;
}
}
private string _name;
public string Name { get => _name; set => SetProperty(ref _name, value); }
private string _searchText;
public string SearchText
{
get => _searchText;
set {
SetProperty(ref _searchText, value);
FilteredOccupationsVisible = false;
}
}
private bool _filteredOccupationsVisible;
public bool FilteredOccupationsVisible { get => _filteredOccupationsVisible; set => SetProperty(ref _filteredOccupationsVisible, value); }
public ObservableCollection<Occupation> _filteredOccupations = new ObservableCollection<Occupation>();
public ObservableCollection<Occupation> FilteredOccupations { get => _filteredOccupations; set { SetProperty(ref _filteredOccupations, value); } }
}
If not using Behaviors in SearchBar , you can have a try with TextChanged method of itself.
<SearchBar x:Name="MySearchBar" Text="SearchText" TextChanged="SearchBar_TextChanged" />
In ContentPage , when text cheanged fire here :
MyViewModel myViewModel = new MyViewModel();
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
Console.WriteLine("new -- " + e.NewTextValue + "-- old -- " + e.OldTextValue);
Console.WriteLine("MyEntry --" + MySearchBar.Text);
//Here can invoke FilterOccupationsList of MyViewModel
myViewModel.FilterOccupationsList(MySearchBar.Text);
}
Else if using Command to do , you need to add isntance of ICommand in MyViewModel to invoke FilterOccupationsList.
public class MyViewModel : BaseViewModel
{
public ICommand FilterOccupationsListCommand { private set; get; }
...
public MyViewModel()
{
FilterOccupationsListCommand = new Command<string>((NewTextValue) =>
{
// Pass value to FilterOccupationsList.
Console.WriteLine("SearchBar new text --" + NewTextValue);
FilterOccupationsList(NewTextValue);
});
}
...
}
In a WPF application, I have a view with an editable datagrid and a viewmodel. This is an existing codebase, and a problem arises: there are fields in the viewmodel that raises exceptions but those fields are not in the view. A refactor is necessary, but for now we need to implement a visual clue (red border around the row) for the user as a quick fix.
From the viewmodel, I raise an event that a validation took place, and in the code-behind, I want to check in the datagridrow if the ValidationErrorTemplate is enabled.
As the elements added by the ValidationErrorTemplate are added as AdornerLayer outside of the datagridrows, it seems that I have no clue to which datagridrow this is coupled?
I have not much code to show, just that I get to the correct datagridrow for which viewmodel a validation took place:
private void OnValidationEvent(ValidationEventArgs e)
{
var rows = BoekingDatagrid.GetDataGridRow(e.ID);
if (e.HasErrors)
{
if (errorBorder == null)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
var vm = row.DataContext as ItemBaseViewModel;
LogValidationErrors(vm, UserContext);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
Every column has the following xaml, with a Validation.ErrorTemplate:
<DataGridTemplateColumn Header="Name"
CanUserResize="False"
SortMemberPath ="Name"
Width="130"
MinWidth="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ViewMode, Converter={StaticResource ViewModeToBoolean}, ConverterParameter=name}"
Validation.ErrorTemplate="{StaticResource ResourceKey=ErrorTemplate2_Grid}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ErrorTemplate2_Grid adds a red border and tooltip to the cell.
In Visual Live Tree, you can see that the rows and the error visuals, but they are not nested:
The question is: how can I find out if there are visual error elements added to the datagridrow, when the viewmodel is invalid?
Not sure what BindingGroup exactly does, but the databound rows does have there own BindingGroups with only the databindings of the controls in the row. And a very convenient property: HasValidationError
private void OnValidationEvent(ValidationEventArgs e)
{
var row = BoekingDatagrid.GetDataGridRow();
if (row != null)
{
if (e.HasErrors)
{
if (!row.BindingGroup.HasValidationError)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
}
else
{
row.BorderThickness = new Thickness(0);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
}
Answered for mine and others future reference.
Validation in WPF is typically done using the interface INotifyDataErrorInfo. There are other ways too, but the ViewModel raising an event that the View handles isn't typically one of them. A very simplified model class could look like this:
public class Model : INotifyDataErrorInfo
{
public Model(int myInt, string myString)
{
MyInt = myInt;
MyString = myString;
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get
{
try
{
return MyInt == 88 || MyString == "foo";
}
catch (Exception)
{
return true;
}
}
}
public int MyInt
{
get { throw new NotImplementedException(); }
set { }
}
public string MyString { get; set; }
public IEnumerable GetErrors(string propertyName)
{
try
{
if (propertyName == nameof(MyInt) && MyInt == 88)
return new string[] { "MyInt must not be 88" };
if (propertyName == nameof(MyString) && MyString == "foo")
return new string[] { "MyString must not be 'foo'" };
return new string[0];
}
catch (Exception)
{
return new string[] { "Exception" };
}
}
}
A quick and diry window could look like this:
public partial class MainWindow : Window
{
public MainWindow()
{
Models = new List<Model>
{
new Model(1,"hello"),
new Model(1,"foo"),
new Model(88,"hello"),
new Model(2,"world"),
};
DataContext = this;
InitializeComponent();
}
public List<Model> Models { get; set; }
}
whereas the XAML just contains <DataGrid ItemsSource="{Binding Models}" />
A red rectangle in case of an error is default. Just apply your custom template and do not set any ValidationErrorTemplate.
I see several other posts about this but I cannot seem to understand exactly how to get this working properly for my usage.
Here is what I have in a nutshell.
I have two Comboboxes--Role and Position.
I have both of these bound to an ObservableCollection which has Enum Values Converted to strings loaded into it on instantiation.
<ComboBox x:Name="empRoleCB" ItemsSource="{Binding Role}" SelectedItem="{Binding RoleStr}"/>
<ComboBox x:Name="empPositionCB" ItemsSource="{Binding Pos}" SelectedItem="{Binding PosStr}"/>
In my ViewModel:
public abstract class EmployeeMenuVMBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if(!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
class EmployeeMenuVM : EmployeeMenuVMBase
{
private ObservableCollection<string> _pos = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Positions)));
private ObservableCollection<string> _role = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Roles)));
public ObservableCollection<string> Pos { get => _pos; }
public ObservableCollection<string> Role { get => _role; }
public string RoleStr
{
get => _roleStr;
set => SetProperty(ref _roleStr, value);
}
public string PosStr
{
get => _posStr;
set => SetProperty(ref _posStr, value);
}
}
What I want to happen is when a Role is selected, based on that selection, only certain Positions should be shown. For instance if I select "Customer Service" as a Role, then Position should only contain "Manager", "CSS" and "None". If Role is "Admin" then Position should only contain "None", and so on and so forth.
The struggle I have is how to filter this properly. I see something with using CollectionViewSource but I am unsure how to get this to work with my example.
I have 5 roles and each role will have a different list of positions that need to be shown.
What is the best way to make this work with MINIMAL extra code or XAML?
One of the things I really dislike about WPF is seemingly simple things need huge amounts of code to make them work properly many times.
First, if you think that WPF is complicated. So, you are using it wrongly.
I suggest you to use the Filter of CollectionViewSource as flow:
<ComboBox x:Name="empPositionCB" ItemsSource="{Binding MyPositionFilter}" SelectionChanged="RoleComboBox_SelectionChanged" ....../>
public ICollectionView MyPositionFilter { get; set; }
//ctor
public MyUserControlOrWindow()
{
//Before InitComponent()
this.MyPositionFilter = new CollectionViewSource { Source = MyPosObservableCollection }.View;
InitComponent();
}
public void RoleComboBox_SelectionChanged(object sender,EventArgs e)
{
//Get the selected Role (the ? is to prevent NullException (VS 2015 >))
Role r = empRoleCB.SelectedItem as Role;
//Apply the filter
this.MyPositionFilter.Filter = item =>
{
//Make you sure to convert correcteley your Enumeration, I used it here like a class
Position p = item as Position;
//Put your condition here. For example:
return r.ToLowers().Contains(p.ToLower());
//Or
return (r != null && r.Length >= p.Length);
};
}
The filter does not change your Collection, All hidden item stay in your ObservableCollection.
This can all be done in your ViewModel by changing the value of the Positions (Pos) observable collection when the role changes.
class EmployeeMenuVM : EmployeeMenuVMBase
{
public EmployeeMenuVM()
{
var emptyPositions = new List<Global.Positions>()
{ Global.Positions.None };
_rolePositions.Add(Global.Roles.None, emptyPositions);
var customerServicePositions = new List<Global.Positions>()
{ Global.Positions.None, Global.Positions.CSS, Global.Positions.Manager };
_rolePositions.Add(Global.Roles.CustomerService, customerServicePositions);
}
private Dictionary<Global.Roles, List<Global.Positions>> _rolePositions = new Dictionary<Global.Roles, List<Global.Positions>>();
private string _roleStr;
private string _posStr;
private ObservableCollection<string> _pos = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Positions)));
private ObservableCollection<string> _role = new ObservableCollection<string>(Enum.GetNames(typeof(Global.Roles)));
public ObservableCollection<string> Pos
{
get => _pos;
set
{
SetProperty(ref _pos, value);
}
}
public ObservableCollection<string> Role
{
get => _role;
}
public string RoleStr
{
get => _roleStr;
set
{
if (SetProperty(ref _roleStr, value))
{
Global.Roles role = (Global.Roles)Enum.Parse(typeof(Global.Roles), value);
var positions = _rolePositions[role].Select(p => p.ToString());
Pos = new ObservableCollection<string>(positions);
}
}
}
public string PosStr
{
get => _posStr;
set => SetProperty(ref _posStr, value);
}
}
Here is a working tester code just to see the main idea of how to do the filtering:
MainWindow.xaml
<Window x:Class="WpfApplication3.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:local="clr-namespace:WpfApplication3"
x:Name="ThisView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="600">
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=Roles, ElementName=ThisView}"
SelectedItem="{Binding Path=SelectedRole, ElementName=ThisView}"
Width="300" Height="60"/>
<ComboBox ItemsSource="{Binding Path=PositionCollectionView, ElementName=ThisView}" Width="300" Height="60"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
public ICollectionView PositionCollectionView { get; set; }
public ObservableCollection<string> Roles { get; set; } = new ObservableCollection<string>();
public ObservableCollection<string> Positions { get; set; } = new ObservableCollection<string>();
private string _selectedRole = String.Empty;
public string SelectedRole
{
get { return _selectedRole; }
set
{
_selectedRole = value;
OnPropertyChanged();
//This Refresh activates the Filter again, so that every time you select a role, this property will call it.
PositionCollectionView.Refresh();
}
}
public MainWindow()
{
PositionCollectionView = CollectionViewSource.GetDefaultView(Positions);
PositionCollectionView.Filter = PositionsFilter;
//use you enums here
Roles.Add("Role1");
Roles.Add("Role2");
Roles.Add("Role3");
Roles.Add("Role4");
Positions.Add("Position1");
Positions.Add("Position2");
Positions.Add("Position3");
Positions.Add("Position4");
InitializeComponent();
}
private bool PositionsFilter(object position)
{
bool result = true;
//place your code according to the Role selected to decide wheather "position" should be in the position list or not
return result;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Hope it helps..
Basic question from a novice. I've been stuck on this and have read through a lot of material and several similar questions on SO; hopefully not a completely duplicate question. I simplified the code as much as I know how to.
I'm trying to make the ListView show a filtered ObservableCollection) property (as the ItemsSource?), based on the selection in the ComboBox.
Specifically, which "meetings" have this "coordinator" related to it.
I'm not seeing any data errors in the output while it's running and debugging shows the properties updating correctly, but the ListView stays blank. I'm trying to avoid any code-behind on the View, there is none currently.
Thanks!
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Meeting> meetings;
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
private string coordinatorSelected;
public string CoordinatorSelected
{
get
{
return coordinatorSelected;
}
set
{
coordinatorSelected = value;
Meetings = fakeDB.Where(v => v.CoordinatorName == CoordinatorSelected) as ObservableCollection<Meeting>;
}
}
private ObservableCollection<string> comboProperty = new ObservableCollection<string> { "Joe", "Helen", "Sven" };
public ObservableCollection<string> ComboProperty
{
get
{
return comboProperty;
}
}
private ObservableCollection<Meeting> fakeDB = new ObservableCollection<Meeting>() { new Meeting("Joe", "Atlas"), new Meeting("Sven", "Contoso"), new Meeting("Helen", "Acme") };
public ObservableCollection<Meeting> ListProperty
{
get
{
return Meetings;
}
}
public class Meeting
{
public string CoordinatorName { get; set; }
public string ClientName { get; set; }
public Meeting(string coordinatorName, string clientName)
{
CoordinatorName = coordinatorName;
ClientName = clientName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Window.Resources>
<local:ViewModel x:Key="VM"></local:ViewModel>
</Window.Resources>
<DockPanel DataContext="{StaticResource ResourceKey=VM}">
<ComboBox Margin="10" ItemsSource="{Binding ComboProperty}" SelectedItem="{Binding CoordinatorSelected}" DockPanel.Dock="Top"/>
<ListView Margin="10" ItemsSource="{Binding ListProperty, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="ClientName"/>
</DockPanel>
Update:
This seems to show that the lambda is returning a Meeting object but the assignment to Meetings is failing. Is this an error in casting maybe?
Thanks again.
You always have to change a property's backing field before you fire a PropertyChanged event. Otherwise a consumer of the event would still get the old value when it reads the property.
Change the Meetings property setter like this:
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
I believe I've found two solutions to the same problem. The error pointed out #Clemens was also part of the solution. The Meetings property problem is solved if I change ListProperty and Meetings to IEnumerable. Alternatively this approach without changing the type, which I believe invokes the collection's constructor with the filtered sequence as an argument.
set
{
coordinatorSelected = value;
var filteredList = fakeDB.Where(v => v.CoordinatorName == coordinatorSelected);
Meetings = new ObservableCollection<Meeting>(filteredList);
OnPropertyChanged("ListProperty");
}
I am trying to build a datagrid where columns are generated dynamically (this works fine) but I am unable to create bindings for the columns which update automatically (something like INotifyPropertyChanged).
Creating columns dynamically and want to use dictionary elements for binding which can be modified/added dynamically. No errors seen in debug output of visual studio.
I think I am really missing something minor here.
clicking button does not populate the second column
ViewModel:
class DataGridAttachedPropertyViewModel {
public ObservableCollection<DataGridColumn> ColumnCollection { get; set; }
public ObservableCollection<AttachedPropertyEmployee> SomEmployees { get; set; }
public ICommand myCommand { get; set; }
public DataGridAttachedPropertyViewModel() {
this.ColumnCollection = new ObservableCollection<DataGridColumn>();
DataGridTextColumn tc = new DataGridTextColumn();
tc.Header = "Sample Column";
// tc.Binding = new Binding("name");
Binding forCurrent = new Binding("SimpleDict[f]");
forCurrent.Mode = BindingMode.TwoWay;
tc.Binding = forCurrent;
DataGridTextColumn tt = new DataGridTextColumn();
tt.Header = "Column x";
// tc.Binding = new Binding("name");
Binding forTheCurrent = new Binding("SimpleDict[x]");
forTheCurrent.Mode = BindingMode.TwoWay;
tt.Binding = forTheCurrent;
myCommand = new DelegateCommand(ButtonBase_OnClick);
this.ColumnCollection.Add(tc);
this.SomEmployees = new ObservableCollection<AttachedPropertyEmployee>();
this.SomEmployees.Add(new AttachedPropertyEmployee("Rajat","Norwalk"));
this.SomEmployees.Add(new AttachedPropertyEmployee("Matthew", "Norwalk"));
}
public void ButtonBase_OnClick() {
foreach (var VARIABLE in SomEmployees) {
VARIABLE.SimpleDict["x"] = "x";
}
}
}
AttachedPropertyEmployee.cs
public class AttachedPropertyEmployee : INotifyPropertyChanged {
private Dictionary<string, string> dict;
public Dictionary<string, string> SimpleDict {
get { return this.dict; }
set
{
if (this.dict != value) {
this.dict = value;
this.NotifyPropertyChanged("SimpleDict");
}
}
}
public AttachedPropertyEmployee(string Name, string Address) {
this.SimpleDict = new Dictionary<string, string>();
SimpleDict["f"] ="b";
this.name = Name;
this.address = Address;
}
public string name;
public string address { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName) {
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
XAML:
<Window x:Class="LearnInteractivity.LearnDataGridAttachedProperty"
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:local="clr-namespace:LearnInteractivity"
mc:Ignorable="d"
Title="LearnDataGridAttachedProperty" Height="300" Width="300">
<!--
Put a datargrid and an attached property and update columns dynamincally.
-->
<StackPanel>
<DataGrid
local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
x:Name="dgg"
AutoGenerateColumns="False"
ItemsSource="{Binding SomEmployees}"></DataGrid>
<Button Content="Populate" Command="{Binding myCommand}"></Button>
</StackPanel>
I see two problems here.
The first is that Dictionary<TKey,TValue> doesn't implement INotifyCollectionChanged, so when you change values in it, no event is raised and the UI never knows about it. You could look for an ObservableDictionary<K,V> and use that (IIRC there are a few implementations around), or you can do it the quick and dirty way:
public void ButtonBase_OnClick() {
foreach (var VARIABLE in SomEmployees) {
VARIABLE.SimpleDict["x"] = "x";
VARIABLE.NotifyPropertyChanged("SimpleDict");
}
}
That will notify the grid that SimpleDict has changed.
The second problem is that in the DataGridAttachedPropertyViewModel constructor, you forgot to add tt to ColumnCollection.
this.ColumnCollection.Add(tc);
this.ColumnCollection.Add(tt);
More thoughts:
I would be more comfortable adding something like this to AttachedPropertyEmployee:
public void SetColumValue(string key, string value) {
SimpleDict[key] = value;
NotifyPropertyChanged("SimpleDict");
}
And use that in your loop instead:
public void ButtonBase_OnClick() {
foreach (var VARIABLE in SomEmployees) {
VARIABLE.SetColumnValue("x", "x");
}
}
Incidentally, I'd change SimpleDict to Dictionary<String, Object> so you can support more types than just string, and leave formatting to the UI. And I might consider exposing a ReadOnlyDictionary<K,V> in the SimpleDict property, with the writable dictionary a private field -- so callers would have no choice but to use SetColumnValue(k,v) to set the column values.