DataGrid not binding (XAML + ViewModel + ObservableCollection) - wpf

New to the WPF + XAML + MVVM stack so I'm sure I'm doing something basic here, but Googling hasn't helped me figure it out. I think A second set of eyes may help.
The Setup
I have a list of Objects called FilesToAdd
I have a DataGrid bound to this list
I have a drag and drop event that fires handling code
I've confirmed this works via Console.WriteLine() output.
The Goal
When an item is added to the list, I'd like the datagrid to be updated with the appropriate information that has just been added to the list.
The Problem
The list seems to be updated, but the datagrid never is.
The Code
Showing only the relevant parts.
UploaderViewModel Class
private ObservableCollection<IAddableFile> _filesToAdd;
public event PropertyChangedEventHandler PropertyChanged;
public UploaderViewModel()
{
_filesToAdd = new ObservableCollection<IAddableFile>();
}
protected virtual void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public ObservableCollection<IAddableFile> FilesToAdd
{
get { return _filesToAdd; }
set
{
if (value != _filesToAdd)
{
_filesToAdd = value;
OnPropertyChanged("FilesToAdd");
OnPropertyChanged("FilesAreQueued");
}
}
}
public bool FilesAreQueued
{
get { return (FilesToAdd.Count > 0); }
}
public void AFileHasBeenAdded(string filepath)
{
var message = String.Format("File dropped: {0}", filepath);
Console.WriteLine(message);
var newFileInfo = new FileInfo(filepath);
if (newFileInfo.Exists && newFileInfo.Length > 0 && (!FileIsADirectory(newFileInfo))) // only add the file to the ViewModel if it's
{
FilesToAdd.Add(new FileSystemFile(newFileInfo)); //Creating our own type becaause we do additional things with it
Console.WriteLine(String.Format("File added to list: {0}", newFileInfo.FullName));
}
}
XAML Binding
<DataGrid ItemsSource="{Binding FilesToAdd}" Height="100" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" MaxHeight="100" AutoGenerateColumns="False" Visibility="{Binding FilesAreQueued, Converter={StaticResource BoolToVisConverter}}">
<DataGrid.Columns>
<DataGridTextColumn Header="File Name" Binding="{Binding FileName}"/>
<DataGridTextColumn Header="Size" Binding="{Binding FileSizeInText}"/>
</DataGrid.Columns>
</DataGrid>
What am I missing? I've been looking at the pattern and I know it has to be something simple I'm not seeing due to staring at a screen for too long. :)

Edit: I suspect the DataGrid updates just fine but you can't see it because the FilesAreQueued property is lying.
You would need something like
FilesToAdd.CollectionChanged += (s,e) =>
OnPropertyChanged("FilesAreQueued");
As you only want to do that once (if at all, can bind to FilesToAdd.Count directly), you really should opt for a readonly collection field.
Looks fine if the DataContext of the view is actually that view-model.
Another issue could be that the class is not implementing INotifyPropertyChanged (you can have the event without actually implementing it using class : interface), this would only apply if you overwrite the FilesToAdd property with a new instance. (In general i expose collections as get-only with a readonly field.)
Might want to check for binding errors (don't think you get any for bindings to a null DataContext though).
(Also i would recommend making the OnPropertyChanged thread-safe, i.e. var handler = <event>; if (handler != null) handler();)

Related

An update to my ObservableCollection doesn't reflect in datagrid

I made a simple example playing with MVVM. I have a simple class Person in Models, then a class PersonsViewModel has a list of Person. I'm doing so by install Prism through Nuget and raise the event PropertyChanged in the collection of people.
private ObservableCollection<Person> people;
public ObservableCollection<Person> People
{
get { return people; }
set
{
people = value;
this.RaisePropertyChanged("People");
}
}
then I bind it to a datagrid
<DataGrid Grid.Row="1" x:Name="dataGrid" AutoGenerateColumns="False" ItemsSource="{Binding People}" >
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName, Mode=OneWay}"/>
<DataGridTextColumn Header="Last Name" Binding="{Binding LastName, Mode=OneWay}"/>
<!--<DataGridTextColumn Header="Sex" Binding="{Binding Sex, Mode=TwoWay}"/>-->
<DataGridComboBoxColumn Header="Sex" SelectedItemBinding="{Binding Sex, Converter={StaticResource sexTypeConverter}}" ItemsSource="{Binding Source={StaticResource sexType}}"/>
<DataGridTextColumn Header="Score" Binding="{Binding Score, Mode=TwoWay}"/>
</DataGrid.Columns>
</DataGrid>
it works find so far, i can insert or remove line, change the quantity in the UI and reflect to the viewmodel.
But the problem is that if I change the quantity in the code, it won't show in the form until I double click into it.
here is the screenshot when my app starts, i've added three records with different scores.
enter image description here
in my code, I've put a command as below, so basically everytime I run the command it will increase the score by 20 for each person. Which it does. the problem is that after I click the button, the score on the form doesn't change, only when i double click into the field score, I can see the number is actually changed.
private void IncQty()
{
foreach (Person item in this.People)
{
item.Score += 20;
}
}
I've tried to search online, some say that I should bind the datasource again. But isn't ObservableCollection already implements the INotifyPropertyChanged and my view should talk to my viewmodel everytime something is changed?
Also ideally I shouldn't do anything in my ViewModel to manipulate the element in View, it seems to break this rule if I write some code to attach the bind again in viewmodel.
Please help. Thanks a lot.
The object contained within your observablecollection needs to implement INotifyPropertyChanged. Without it none of the UI components (and the ObservableCollection itself) can know something has changed and therefore need to refresh.
e.g.:
public abstract class ObservableBase : INotifyPropertyChanged
{
private PropertyChangedEventHandler _notifyPropertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add { this._notifyPropertyChanged = (PropertyChangedEventHandler)Delegate.Combine(this._notifyPropertyChanged, value); }
remove { this._notifyPropertyChanged = (PropertyChangedEventHandler)Delegate.Remove(this._notifyPropertyChanged, value); }
}
protected void OnPropertyChanged(string property)
{
if(this._notifyPropertyChanged != null)
{
this._notifyPropertyChanged.Invoke(property);
}
}
}
public class Person : ObservableBase
{
private int _score;
public int Score
{
get => this._score;
set
{
if(this._score != value)
{
this._score = value;
// would be better if mixed with reflection and Expression
// so you can this.OnPropertyChanged(p => p.Score);
// as per Rockford Lhotka & CSLA
this.OnPropertyChanged("Score");
}
}
}
}
As a result when you set the score property, property changed will fire, which should mean the datarow component it is bound to will hear it. Even if not the observable collection listens to see if it's T is also INotifyPropertyChanged and will fire an event of its own letting the UI know that the item has changed. Either way your WPF will now reflect the changed applied to score.

WPF: Disable the selected item in ComboBox

I have been going through posts for 3 hours now with no resolution. I am new to WPF and created the ComboBox below:
Unfortunately I cannot disable the highlighting of the selected item. Does anyone have a viable solution?
Code:
<StackPanel Grid.Column="1"
Margin="800,0,0,0"
Width="135"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<ComboBox Name="LangComboBox"
IsEditable="True"
IsReadOnly="True"
Text="Select Language">
<ComboBoxItem>English</ComboBoxItem>
<ComboBoxItem>Spanish</ComboBoxItem>
<ComboBoxItem>Both</ComboBoxItem>
</ComboBox>
</StackPanel>
I would like to clarify first of all that mine wants to be constructive answer and want to try to spread the culture of good programming.
We all have always to learn about programming, me too!
If you do not know a topic, it is good practice to study perhaps starting from a good book or from the official documentation of the platform.
That said let's move on to some possible approaches to your problem.
First of all, the fact that the selection in the combobox is that way is due to the basic template of the combobox that I invite you to view: https://msdn.microsoft.com/library/ms752094(v=vs.85).aspx )
What you are looking for is a different behavior of the combobox:
Allow display of a default value
Once an element is selected, the text inside it is not underlined
A first approach could be based on the ComboBox template: the combobox is constructed in such a way that, if it is editable, its template
contains a textbox called PART_EditableTextBox
by acting on the textbox, for example by making it disabled, you can get the result you want.
And this can be implemented in different ways:
Inserting a code-behind event handler that disables the textbox when the combobox is loaded
With an Attached behavior that allows you to add custom behaviors to the controls (https://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF)
Write a custom control that maybe insert a watermark type part to your combobox
Now consider the first approach that is the fastest to implement so the code could be the following:
<ComboBox Name="LangComboBox" IsEditable="True" IsReadOnly="True"
Loaded="LangComboBox_Loaded"
Text="Select language">
<ComboBoxItem Content="English"/>
<ComboBoxItem Content="Spanish"/>
<ComboBoxItem Content="Both"/>
</ComboBox>
In the code-behind:
private void LangComboBox_Loaded(object sender, RoutedEventArgs e)
{
ComboBox ctrl = (ComboBox)sender;
TextBox Editable_tb = (TextBox)ctrl.Template.FindName("PART_EditableTextBox", ctrl);
if (Editable_tb != null)
{
// Disable the textbox
Editable_tb.IsEnabled = false;
}
}
This approach, however, has drawbacks, among which the fact that if the user wants to deselect / reset the value of the combo can not do it.
So you could follow another path using the MVVM pattern.
Coming from the world of web programming you should know the MVC pattern, in WPF the most common pattern is MVVM or Model - View - ViewModel
between the two patterns there are different things in common and I invite you to take a look at them: Mvvm Pattern.
You could create a class with the model that will be hosted in the combo for example:
public class Language
{
public int Id { get; set; }
public string Description { get; set; }
public Language(int id, string desc)
{
this.Id = id;
this.Description = desc;
}
}
public class YourDataContext : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private List<Language> _Languages;
public List<Language> Languages
{
get
{
return _Languages;
}
set
{
_Languages = value;
OnPropertyChanged("Languages");
}
}
private Language _selectedLanguage;
public Language SelectedLanguage
{
get
{
return _selectedLanguage;
}
set
{
_selectedLanguage = value;
OnPropertyChanged("SelectedLanguage");
}
}
public YourDataContext()
{
// Initialization of languages
Languages = new List<Language>();
Languages.Add(new Language(0, "None - Select a Language"));
Languages.Add(new Language(1, "English"));
Languages.Add(new Language(2, "Spanish"));
Languages.Add(new Language(3, "Both"));
SelectedLanguage = Languages.First();
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
// some other properties and commands
}
// Your Window class
public MainWindow()
{
InitializeComponent();
var dc = new YourDataContext();
DataContext = dc;
}
<ComboBox ItemsSource="{Binding Languages}"
DisplayMemberPath="Description"
SelectedItem="{Binding SelectedLanguage}"/>
Note that now the combobox is no longer editable and it is possible to reset the selection.
You can manage the selection using the model:
if(dc.SelectedLanguage.Id == 0)
{
//No language selected
}
There are a lot of different ways to achieve what you want, i hope this gave you some good point to start from.
Good programming to everyone.

Need help: DataGridCheckBoxColumn two-way works only one-way

I (..still learning wpf..) made a DataGrid with the first column showing that a signal is enabled or not.
In the xaml:
<DataGrid.Columns> ...
<DataGridCheckBoxColumn Width ="30" Header="" IsReadOnly="False" Binding="{Binding IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
The ItemSouce of DataGrid is correctly set and bound with the data in list of ObservableCollection<Signal> signalList. Everything is correctly shown in DataGrid. So the binding from signalList to DataGrid here is working fine.
On the other side, I want that with every change of signalList, the DataGrid can update itself automatically. However, if I do
signalList[0].IsEnabled = true;
The DataGrid doesn't get updated. I searched a lot but still can't find the answer.
Did I miss something? Thx.
Edit1:
DataGrid do get updated, only if I click another row, and then draw the scroller out of sight. Then if I draw the scroller back, the row is correctly shown. I think I definitively missed something, can someone give me a hint?
I solved the problem with help from Aybe and gavin. For record, I add my code here:
class Signal : INotifyPropertyChanged
{
...
private bool isEnabled;
public bool IsEnabled
{
get { return isEnabled; }
set { isEnabled = value; OnPropertyChanged(new PropertyChangedEventArgs("IsEnabled"));}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}

access TextBoxes inside DataTemplate

I know question similar to this has asked several times in SO. But non of them address my issue and have some difficulty with understanding those answers. This is my situation; I have a ItemsControl I have used ItemTemplate and bound some data.
<Window.Resources>
<DataTemplate x:Key="AdditionalFieldTemlate">
<Grid>
<TextBlock Text="{Binding InfoName}"/>
<TextBox Text="{Binding InfoValue,Mode=TwoWay}" Name="CustomValue"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding AdditionalInformation}" x:Name="additionalInfo" ItemTemplate="{DynamicResource AdditionalFieldTemlate}"/>
</Grid>
I need to set TextBox text to empty(all the textboxes text inside datatemplate) once click on a Button. Don't know how to access these textboxes. Please help me.
You don't normally access the TextBoxes (the look) ....you access the data that is being bound to.
So you can just alter the "data" in your collection as follows:
foreach (var item in AdditionalInformation)
{
item.InfoValue = "";
}
The "TextBoxes" will then be emptied.
Make sure you have implemented INotifyPropertyChanged on the class being used by AdditionalInformation....so that when the InfoValue property is altered it raises a notification.
The text in the textboxes is databound to the InfoValue property of your class. Implement the class and proprty like this:
class InfoClass: INotifyPropertyChanged
{
private string _infoValue;
...
public string InfoValue
{
get { return _infoValue; }
set
{
_infoValue = value;
OnNotifyPropertyChanged("InfoValue")
}
}
...
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Then do what colinsmith suggests in your button click handler (or command if you went with the MVVM approach). The binding will be notified to the change and the view will be updated.

WPF DataGrid multiselect binding

I have a datagrid that is multi-select enabled. I need to change the selection in the viewmodel. However, the SelectedItems property is read only and can't be directly bound to a property in the viewmodel. So how do I signal to the view that the selection has changed?
Andy is correct. DataGridRow.IsSelected is a Dependency Property that can be databound to control selection from the ViewModel. The following sample code demonstrates this:
<Window x:Class="DataGridMultiSelectSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
Title="Window1" Height="300" Width="300">
<StackPanel>
<tk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}" EnableRowVirtualization="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Value" Binding="{Binding Value}" />
</tk:DataGrid.Columns>
<tk:DataGrid.RowStyle>
<Style TargetType="tk:DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</tk:DataGrid.RowStyle>
</tk:DataGrid>
<Button Content="Select Even" Click="Even_Click" />
<Button Content="Select Odd" Click="Odd_Click" />
</StackPanel>
</Window>
using System.ComponentModel;
using System.Windows;
namespace DataGridMultiSelectSample
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new[]
{
new MyViewModel {Value = "Able"},
new MyViewModel {Value = "Baker"},
new MyViewModel {Value = "Charlie"},
new MyViewModel {Value = "Dog"},
new MyViewModel {Value = "Fox"},
};
}
private void Even_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[]) DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i%2 == 0;
}
private void Odd_Click(object sender, RoutedEventArgs e)
{
var array = (MyViewModel[])DataContext;
for (int i = 0; i < array.Length; ++i)
array[i].IsSelected = i % 2 == 1;
}
}
public class MyViewModel : INotifyPropertyChanged
{
public string Value { get; set; }
private bool mIsSelected;
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (mIsSelected == value) return;
mIsSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Be sure to set EnableRowVirtualisation="False" on the DataGrid element, else there's a risk that the IsSelected bindings fall out of kilter.
I haven't worked with the DataGrid much, but one technique that works for the ListView is to bind to the IsSelected property of the individual ListViewItem. Just set this to true for each object in your list, and then it will get selected.
Maybe the object that represents a row in the DataGrid also has an IsSelected property, and can be used in this way as well?
Guys, thanks for the help. My problem was solved. I think the problem is pretty common for new WPF developers, so I will restate my problem and as well as the solution in more details here just in case someone else runs into the same kind of problems.
The problem: I have a multi-select enabled datagrid of audio files. The grid has multiple column headers. The user can multi-select several row. When he clicks the Play button, the audio files will be played in the order of one the columns headers (say column A). When playback starts, the multi-select is cleared and only the currently playing file is highlighted. When playback is finished for all files, the multi-selection will be re-displayed. The playback is done in the viewmodel. As you can see, there are two problems here: 1) how to select the currently playing file from the viewmodel, and 2) how to signal to the view from the viewmodel that playback is finished and re-display the multi-selection.
The solution: To solve the first problem, I created a property in the viewmodel that is bound to the view's SelectedIndex property to select the currently playing file. To solve the second problem, I created a boolean property in the view model to indicate playback is finished. In the view's code behind, I subscribed the the boolean property's PropertyChanged event. In the event handler, the view's SelectedItems property is re-created from the saved multi-selection (the contents of SelectedItems was saved into a list and SelectedItems was cleared when playback started). At first, I had trouble re-creating SelectedItems. It turned out the problem was due to the fact that re-creation was initiated through a second thread. WPF does not allow that. The solution to this is to use the Dispatcher.Invoke() to let the main thread do the work. This may be a very simple problem for experienced developers, but for newbies, it's a small challenge. Anyway, a lot of help from different people.
Just use SelectedItems on any MultiSelector derived class , and use methods Add, Remove, Clear on IList it returns .

Resources