I'm using Caliburn Micro on a Windows Store App.
I have a ListView which has a SelectedItem that works the first time I use it. However, when I clear the ListView and re-bind it to another Collection, the selected item no longer appears selected.
The selectedItem property is being set correctly, since I can hit the breakpoint, and everything works has expected, just the View is not being updated with the selected item, after I clear the collection.
What could be wrong?
Thanks.
Edit:
View Code:
<ListView x:Name="DetailNotes"
ItemsSource="{Binding DetailNotes}"
SelectedItem="{Binding SelectedDetailNote}"
ItemTemplate="{StaticResource Notes600ItemTemplate}"
IsItemClickEnabled="True"
caliburn:Message.Attach="[Event ItemClick] = [DetailNoteSelected($eventArgs)]"/>
ViewModel Code:
(...)
private Note selectedDetailNote;
public Note SelectedDetailNote
{
get { return this.selectedDetailNote; }
set
{
this.selectedDetailNote = value;
this.NotifyOfPropertyChange(() => this.SelectedDetailNote);
}
}
(...)
public void DetailNoteSelected(ItemClickEventArgs eventArgs)
{
Note n = (Note)eventArgs.ClickedItem;
this.SelectedDetailNote = n;
}
Sorry! The problem was my explicit binding. I just left Caliburn do his work, and now it works!
Solution below:
View Code:
<ListView x:Name="DetailNotes"
ItemTemplate="{StaticResource Notes600ItemTemplate}"/>
ViewModel Code:
private Note selectedDetailNote;
public Note SelectedDetailNote
{
get { return this.selectedDetailNote; }
set
{
this.selectedDetailNote = value;
this.NotifyOfPropertyChange(() => this.SelectedDetailNote);
}
}
I know it's late, but your problem was binding mode. You should set it to TwoWay:
SelectedItem="{Binding SelectedDetailNote, Mode=TwoWay}"
In WinRT XAML default is OneWay.
Related
I'm trying to bind an ObservableCollection<T> to a DataGrid in WPF.
Below the DataGrid, there are fields to edit the currently selected item from the DataGridlike so:
So the generic T of the ObservableCollection<T> has the following properties:
- Title (Überschrift)
- Description (Beschreibung)
- Path (Pfad)
and it also has a property Reihenfolge which means Order.
With the yellow arrows, I want to be able to modify the order of the entries.
Unfortunately, the ObservableCollection doesn't have an OrderBy-method...
I've tried the following:
In XAML I have defined a CollectionViewSource like this:
<CollectionViewSource Source="{Binding Bilder}" x:Key="Pictures">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Reihenfolge" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
And I have binded the DataGrid to this CollectionViewSource
<DataGrid Grid.Column="0" Grid.Row="1"
Name="PictureDataGrid"
ItemsSource="{Binding Source={StaticResource Pictures}}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="false"
SelectedItem="{Binding SelectedBild}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
...
In the ViewModel, I have these properties:
public ObservableCollection<BildNotifiableModel> Bilder { get; set; }
public BildNotifiableModel SelectedBild { get; set; }
and two methods which are called with DelegateCommands that update the order
private void MoveSeiteUp()
{
const int smallestReihenfolge = 1;
if (this.SelectedBild.Reihenfolge > smallestReihenfolge) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge - 1);
this.SelectedBild.Reihenfolge--;
bildToSwapReihenfolgeWith.Reihenfolge++;
RaisePropertyChanged(nameof(this.Bilder));
}
}
private void MoveSeiteDown()
{
if (this.SelectedBild.Reihenfolge < MaxAllowedImages) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge + 1);
this.SelectedBild.Reihenfolge++;
bildToSwapReihenfolgeWith.Reihenfolge--;
RaisePropertyChanged(nameof(this.Bilder));
}
}
The order gets updated correctly, but unfortunately, the view doesn't reflect the changes... only after closing and reopening the view, the entries in the DataGrid are in the correct order.
What am I doing wrong here?
How can I make the DataGrid update, when changing the order?
Thanks in advance
I think the problem is that the CollectionView doesn't listen for the PropertyChanged-Events from its elements and also RaisePropertyChanged(nameof(this.Bilder)); dosen't work because the CollectionView is not really changed.
I would recomend to create the CollectionView in code via CollectionViewSource.GetDefaultView(list). So you can control the CollectionView from your model and call ICollectionView.Refresh if needed.
In your Methods, create a new Collection and add it to "Bilder". Just raising the PropertyChanged will execute an evaluation for referential equality. If it is the same - which it will be, if you just move items inside around - it will not update the DataGrid.
If you are not using the ObservableCollections attributes, like automatically updates, when items are added or removed, you might also change it to a "normal" List.
private void MoveSeiteUp()
{
const int smallestReihenfolge = 1;
if (this.SelectedBild.Reihenfolge > smallestReihenfolge) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge - 1);
this.SelectedBild.Reihenfolge--;
bildToSwapReihenfolgeWith.Reihenfolge++;
this.Bilder = new ObservableCollection<BildNotifiableModel> (this.Bilder);
RaisePropertyChanged(nameof(this.Bilder));
}
}
private void MoveSeiteDown()
{
if (this.SelectedBild.Reihenfolge < MaxAllowedImages) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge + 1);
this.SelectedBild.Reihenfolge++;
bildToSwapReihenfolgeWith.Reihenfolge--;
this.Bilder = new ObservableCollection<BildNotifiableModel> (this.Bilder);
RaisePropertyChanged(nameof(this.Bilder));
}
}
Question summary: Is there a way in XAML to ensure that my DataGrid component is fully loaded before it initiates a binding on the SelectedIndex property?
My ViewModel is set up like this. I'm using MVVM-light to notify the view of changes. I pass the new model to SetData() whenever the it gets updated from the server.
public class MyViewModel : ViewModelBase
{
public void SetData(DataModel model)
{
Data = model.Data; //Array of 75 DataObjects
DataIndex = model.Index; //int between 0 and 74
}
// Array to Bind to Datagrid ItemsSource
public DataObject[] Data
{
get { return _data; }
private set
{
if (_data!= value)
{
_data= value;
RaisePropertyChanged("Data");
}
}
}
private DataObject[] _data;
// Int to Bind to Datagrid SelectedIndex
public int DataIndex
{
get { return _index; }
private set
{
if (_index != value)
{
_index = value;
RaisePropertyChanged("DataIndex");
}
}
}
private int _index;
}
The view looks like this:
<Application.Resources>
<ResourceDictionary>
<core:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>
</Application.Resources>
<DataGrid ItemsSource="{Binding MyViewModel.Data, Source={StaticResource Locator}}"
SelectedIndex="{Binding MyViewModel.DataIndex, Source={StaticResource Locator}, Mode=OneWay}"
AutoGenerateColumns="True"/>
My issue is that none of the rows are selected on my DataGrid. All the data shows up in the grid correctly but the row does not get selected. I've checked the properties and confirmed that the Array length is 75 and DataIndex is an int between 0 and 74.
It seems the reason is because the DataGrid hasn't finished loading when the binding is set. I can prove this by initializing the binding after the component is loaded. In this case, everything works as expected and my selected item is displayed correctly:
<DataGrid x:Name="MyDataGrid" Loaded="OnDataGridLoaded"
ItemsSource="{Binding MyViewModel.Data, Source={StaticResource Locator}}"
AutoGenerateColumns="True"/>
private void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
Binding b = new Binding("DataIndex");
b.Source = Locator.MyViewModel.Data;
MyDataGrid.SetBinding(DataGrid.SelectedIndexProperty, b);
}
I'd prefer not to have to do it like this because, you know, code-behind. So is there a way to fix this using only XAML? Here is what I've tried so far (none of which worked for me):
Binding SelectedIndex to an int property on my ViewModel (as shown above)
Binding SelectedItem to a DataObject property on my ViewModel (same result)
Binding SelectedValue and SelectedPath to a property of my DataObject (This actually worked, for the first instance only. The problem is I have multiple instances of this Datagrid component, and for some reason this only works on the first instance)
Binding to an ObservableCollection instead of an Array (tried all 3 of the above methods with an ObservableCollection and got the same results for each)
Delaying the change notification by wrapping it in a call to Dispatcher.Invoke. This doesn't help because the component is not immediately in view.
Creating the binding in XAML and then updating the target in the Loaded function. MyDataGrid.GetBindingExpression(DataGrid.SelectedIndexProperty).UpdateTarget();
My original question was missing a piece of info that was causing the issue.
It seems there's a WPF bug when the binding source is a static link. If I move the DataIndex property into the component's DataContext, then it will work correctly.
I'm not going to do this however, because the data is shared between multiple instances. I don't need to have multiple instances of the data, only the component. Therefor, I will just right it off as a Microsoft bug, and use the code-behind work around.
I will leave the question open tho, in case anyone has a solution for this bug.
This is for a Windows 10 Universal App.
XAML:
<RelativePanel Padding="4" Margin="4,12,0,0">
<TextBlock x:Name="Label" Text="Class Name" Margin="12,0,0,4"/>
<ListView x:Name="ClassTextBoxes"
ItemsSource="{Binding TextBoxList}"
SelectionMode="None" RelativePanel.Below="Label">
<ListView.ItemTemplate>
<DataTemplate >
<RelativePanel>
<TextBox x:Name="tbox"
PlaceholderText="{Binding PlaceHolder}"
Text="{Binding BoxText,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Padding="4" Width="200" MaxLength="25"/>
<TextBlock x:Name="errorLabel"
RelativePanel.Below="tbox"
Text="{Binding Error, Mode=TwoWay}"
Padding="0,0,0,4"
FontSize="10"
Foreground="Red"/>
<Button Content="Delete" Margin="12,0,0,0" RelativePanel.RightOf="tbox"/>
</RelativePanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</RelativePanel>
Model:
public class TextBoxStrings : BaseModel
{
private string _placeholder;
public string PlaceHolder
{
get { return _placeholder; }
set
{
if (_placeholder != value)
{
_placeholder = value;
NotifyPropertyChanged();
}
}
}
private string _boxText;
public string BoxText
{
get { return _boxText; }
set
{
if (_boxText != value)
{
_boxText = CheckBoxText(value);
NotifyPropertyChanged();
}
}
}
public string CheckBoxText(string val)
{
var r = new Regex("[^a-zA-Z0-9]+");
return r.Replace(val, "");
}
}
ViewModel:
private TrulyObservableCollection<TextBoxStrings> _textBoxList;
public TrulyObservableCollection<TextBoxStrings> TextBoxList
{
get { return _textBoxList; }
set
{
if (_textBoxList != value)
{
_textBoxList = value;
RaisePropertyChanged();
}
}
}
and I add new TextBoxString objects to my TextBoxList collection from within my view-model.
I want to make it that users can't type in certain characters (or rather, they get deleted whenever they
are typed in.
This works...in the model. Setting breakpoints and looking at the values, everything in the Model is working: value goes into the setter and gets changed, _boxText holds the new value that is set from CheckBoxText();
But the problem is, in my View, the textbox doesn't reflect changes to the underlying text that I make in the model.
So if I type in "abc*()" into "tbox", the value in the model will be "abc". The value of the textbox, however, will still be "abc*()".
I have a feeling it has something to do with the fact that I'm editing items that are inside of a collection and I don't have anything implemented to handle changing items within a collection. I was under the impression that using INotifyPropertyChanged and ObservableCollection<T> would take care of that for me.
Does anyone have any suggestions?
Thank you!
Edit: So, now I'm trying to use TrulyObservableCollection because I thought this was the problem, but it hasn't helped. Here it is: https://gist.github.com/itajaja/7507120
But the problem is, in my View, the textbox doesn't reflect changes to the underlying text that I make in the model.
As you've seen, the TextBox do reflect changes to your model. When you type in "abc*()" in the TextBox, the value in the model will be changed to "abc". The problem here is that the binding system in UWP is "intelligent". For TwoWay bindings, changes to the target will automatically propagate to the source and in this scenario, binding system assumes that the PropertyChanged event will fire for corresponding property in source and it ignores these events. So even you have RaisePropertyChanged or NotifyPropertyChanged in you source, the TextBox still won't update.
In WPF, we can call BindingExpression.UpdateTarget Method to force the update. But this method is not available in UWP.
As a workaround, you should be able to use TextBox.TextChanged event to check the input like following:
private void tbox_TextChanged(object sender, TextChangedEventArgs e)
{
var tb = sender as TextBox;
if (tb != null)
{
var originalText = tb.Text;
var r = new Regex("[^a-zA-Z0-9]+");
if (originalText != r.Replace(originalText, ""))
{
var index = (tb.SelectionStart - 1) < 0 ? 0 : (tb.SelectionStart - 1);
tb.Text = r.Replace(originalText, "");
tb.SelectionStart = index;
}
}
}
However it may break your MVVM model, you can use data validation to avoid this and here is a blog: Let’s Code! Handling validation in your Windows Store app (WinRT-XAML) you can refer to. And for my personal opinion, data validation is a better direction for this scenario.
if (_boxText != value)
{
_boxText = CheckBoxText(value);
NotifyPropertyChanged();
}
Try changing this to:
var tmp = CheckBoxText(value);
if (_boxText != tmp)
{
_boxText = tmp;
NotifyPropertyChanged();
}
I hope, in your XAML, the binding to property BoxText is two-way, right?
You should edit BoxText and then send checked value to UI. Just send value to CheckBoxText and already edited should be assigned to _boxText. And then you should send BoxText to UI by calling RaisePropertyChanged("BoxTest"). Please, see the following code snippet:
private string _boxText;
public string BoxText
{
get { return _boxText; }
set
{
if (_boxText != value)
{
_boxText=CheckBoxText(value);
RaisePropertyChanged("BoxText");
}
}
}
There is no difference where you use INotifyPropertyChanged for one property of for properties placed in collection. The complete example with collections and ListView can be seen here
I have created a combobox in xaml like this:
ComboBox x:Name="cbTest" SelectedValue="{Binding TestSpeed}" HorizontalAlignment="Left" Margin="0,10,0,0" Width="250" SelectionChanged="cbTest_SelectionChanged"/>
And the Combobox is filled with the following items:
for (int i = 1; i < 6; i++)
cbTest.Items.Add(i);
I see the items in the combobox, but it doesn't show the SelectedValue what I choose before. This is the property:
private short _testSpeed;
public short TestSpeed
{
get
{
return _testSpeed;
}
set
{
_testSpeed= value;
NotifyPropertyChanged();
}
}
And this is when I change the item on SelectedChanged
_vm.TestSpeed = (short)Convert.ToInt16(cbTest.SelectedValue);
TestSpeed gives me the correct data in debug, but the selectedValue binding isn't working!?
In your situation binding mode must be OneWayToSource like SelectedValue="{Binding Path=TestSpeed, Mode=OneWayToSource} . When TestSpeed is string or int that is work, when short - not work. I think you need to write specific converter for using short or using int and dont worry
I want when a user selects one or multiple items that my source property gets updated. I have tried with the binding mode OneWayToSource but this is not helping. Below is the XAML and ViewModel code:
<ListBox x:Name="ItemsListBox" SelectionMode="Multiple" Height="300"
ItemsSource="{Binding ResultSet}"
SelectedItem="{Binding SelectedItems,Mode=OneWayToSource}">
private List<string> _selectedItems;
public List<string> SelectedItems
{
get
{
return _selectedItems;
}
set
{
_selectedModeItems = value;
NotifyPropertyChanged("SelectedItems");
}
}
I have taken the approach by using Attached behaviours and it works , but is there any simpler way?
Your question should be like this.
How to get multiple selected items from the ListBox in WPF with MVVM?
Well, you have the answer from following stackoverflow threads.
link 1
link 2
Simply you can define IsSelected property in your ResultSet view model. Then if you want to get selected items at any point, just get the items which the "IsSelected" property is set to true from the ResultSet.
you could also create an Attached Behavior
here is an Example how to do it
WPF ListBox has two properties related to the currently selected item:
SelectedItem available for binding, bound to the first selected item.
SelectedItems (with an 's' at the end) is not available to binding.
When multi selection is enabled, you want to have access to SelectedItems but unfortunately you can't bind to it
You can workaround this limitation using code behind.
Create a property named SelectedItems that will contain the selection, then subscribe the SelectionChanged event:
<ListBox x:Name="ItemsListBox" SelectionMode="Multiple" Height="300"
ItemsSource="{Binding ResultSet}"
SelectionChanged="ListBox_SelectionChanged">
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (string item in e.RemovedItems)
{
SelectedItems.Remove(item);
}
foreach (string item in e.AddedItems)
{
SelectedItems.Add(item);
}
}