I have a listbox in a Silverlight application.
The listbox:
<ListBox Grid.Row="1" Grid.ColumnSpan="2" Name="lbHazards" Margin="5"
MinHeight="75" ItemsSource="{Binding Path=HazListByTaskIDCollection}"
DisplayMemberPath="sHaz_Name"
IsEnabled="{Binding Path=IsEnabled}" />
In the view model I have
Private _HazListByTaskIDCollection As ObservableCollection(Of vw_HazList_By_TaskID)
Public Property HazListByTaskIDCollection() As ObservableCollection(Of vw_HazList_By_TaskID)
Get
Return _HazListByTaskIDCollection
End Get
Set(ByVal value As ObservableCollection(Of vw_HazList_By_TaskID))
_HazListByTaskIDCollection = value
'Used to notify CommonBase class that a property change has occured
RaisePropertyChanged("HazListByTaskIDCollection")
End Set
End Property
Then I have a sub in the viewmodel:
Public Sub FillHazList(ByVal iHazID As Integer, ByVal sHaz_Name As String)
Try
Dim yy = New vw_HazList_By_TaskID
yy.iHazID = iHazID
yy.sHaz_Name = sHaz_Name
HazListByTaskIDCollection.Add(yy)
Catch ex As Exception
DisplayError("Error Happened", ex)
End Try
End Sub
And that works perfectly when this Sub is called the item is added to the listbox.
But I also need to be able to remove the items from the listbox
So I thought it would be easy enough so I created another sub
Public Sub RemoveHazListItem(ByVal iHazID As Integer, ByVal sHaz_Name As String)
Try
Dim yyy = New vw_HazList_By_TaskID
yyy.iHazID = iHazID
yyy.sHaz_Name = sHaz_Name
HazListByTaskIDCollection.Remove(yyy)
HazListByTaskIDCollection.Clear()
Catch ex As Exception
DisplayError("Error Happened", ex)
End Try
End Sub
This runs with no errors but it does NOT remove the item from the listbox.
What am I doing wrong?
Your RemoveHazListItem method appears to be creating a new vw_HazList_By_TaskID object, putting a couple of values in it, and attempting to remove this newly-created object from your collection. It seems you're not getting the behaviour you are expecting because you're attempting to remove from a collection an item that was never added to it.
As far as I can see, there are two solutions to your problem:
Override Equals in your vw_HazList_By_TaskID class. Doing this should allow new objects to be considered equal to existing objects in the collection, and hence you should be able to remove objects from the collection by passing to Remove an object that is equal to the one you want to remove.
Look through the collection for a vw_HazList_By_TaskID object with matching iHazID and sHaz_Name properties, and remove that object from the collection instead.
Incidentally, the Remove method of the ObservableCollection(Of T) class returns a Boolean value indicating whether it was able to remove a value from the list. In the event of the item to remove not being found, it returns False as opposed to throwing an exception.
Try like this:
HazListByTaskIDCollection.RemoveAt(HazListByTaskIDCollection.IndexOf(yyy));
Related
Imagine a WPF project with an MVVM approach. So, there is a view and a view model.
In the view model I have a property, that might throw an exception in the setter.
Public Property DateValue As Nullable(Of Date)
Get
Return _dateValue
End Get
Set(value As Nullable(Of Date))
If value.HasValue Then
If value.Value < Date.Today Then
Throw New Exception("Error Message")
End If
End If
_dateValue = value
'skipped NotifyPropertyChanged in this example for the sake of simplicity
End Set
End Property
In the view there is a control bound to this property. And since I like to see my exceptions I switched on ValidatesOnExceptions in the binding and add an ErrorTemplate.
...
<DatePicker SelectedDate="{Binding DateValue, ValidatesOnExceptions=True}"
SelectedDateFormat="Short"
Validation.ErrorTemplate="{StaticResource ErrorTemplate}" />
...
Since you can't reset the value of a DatePicker once you picked one (at least I don't know how to do that) I added a small reset button right next to the DatePicker which is bound to a command which sets the property DateValue of the view model to Nothing. And since I don't want to see this button all the time I bound its Visibility to DateValue.HasValue, so this button only shows, when there's a value to reset.
So far, so good.
But now I have a problem when I pick an invalid date in the DatePicker (one that throws an exception in the properties' setter).
My reset button doesn't show up, since there's no value in the bound property, and I can't reset the DatePicker any other way (at least not that I know of). I'd first have to pick a proper date before I can reset the whole thing.
So, is there any way to determine, whether my property setter threw an exception. There must be a way, since this very error is shown to the user.
Or do I have to manually remember, that I threw an exception in another variable, to be able to access this information when needed?
And how do I "clear" the DatePicker. Since there's no value in the property, setting the property to Nothing will not change anything in the view. How would I get rid of the error?
Class ViewModel
Inherits INotifyPropertyChanged
Private selectedDate As DateTime
Public Property SelectedDate As DateTime
Get
Return Me.selectedDate
End Get
Set(ByVal value As DateTime)
If value < DateTime.Today Then
Throw New ArgumentException("Date can't be in the past.")
End If
Me.selectedDate = value
OnPropertyChanged()
End Set
End Property
Public ReadOnly Property ResetDateCommand As ICommand
Get
Return New RelayCommand(AddressOf ExecuteResetDate)
End Get
End Property
Public Sub New()
Me.SelectedDate = DateTime.Today
End Sub
Public Sub ExecuteResetDate(ByVal commandParameter As Object)
Return CSharpImpl.__Assign(Me.SelectedDate, DateTime.Today)
End Sub
End Class
MainWindow.xaml
<Window>
<Window.Resources>
<ViewModel />
</Window.Resources>
<StackPanel x:Name="RootPanel" viewModels:Item.IsMarkedAsRead="True">
<Button Content="Reset Date"
Visibility="{Binding ElementName=DatePicker, Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilityConverter}}"
Command="{Binding ResetDateCommand}" />
<DatePicker x:Name="DatePicker"
SelectedDate="{Binding SelectedDate, ValidatesOnExceptions=True}"
SelectedDateFormat="Short" />
</StackPanel>
</Window>
Remarks
It's best practice in UI design that you always prevent wrong input. If possible you shouldn't allow invalid input e.g. by disabling buttons, hiding invalid options, limiting ranges etc.
Don't allow the user to select invalid options. This leads to a frustrating user experience. One way to prevent wrong input is to use specialized controls. Instead of forcing the user to type a date you offer a DatePicker. This eliminates typos. But it can also eliminate wrong selections by providing only valid dates.
If you want to disallow selecting dates of the past you can narrow down the selectable range:
<!-- Only show dates from today -->
<DatePicker DisplayDateStart="{x:Static system:DateTime.Today}" />
If you have additional illegal dates e.g. holidays you can define a collection of those dates by setting the DatePicker.BlackoutDates property:
DatePickerHelper.cs
Class DatePickerHelper
Inherits DependencyObject
Public Shared ReadOnly BlackedDaysProperty As DependencyProperty = DependencyProperty.RegisterAttached("BlackedDays", GetType(IEnumerable(Of CalendarDateRange)), GetType(DatePickerHelper), New PropertyMetadata(Nothing, AddressOf DatePickerHelper.OnBlackDatesChanged))
Public Shared Sub SetBlackedDays(ByVal attachingElement As DependencyObject, ByVal value As IEnumerable(Of CalendarDateRange))
Return attachingElement.SetValue(DatePickerHelper.BlackedDaysProperty, value)
End Sub
Public Shared Function GetBlackedDays(ByVal attachingElement As DependencyObject) As IEnumerable(Of CalendarDateRange)
Return CType(attachingElement.GetValue(DatePickerHelper.BlackedDaysProperty), IEnumerable(Of CalendarDateRange))
End Function
Private Shared ReadOnly Property DatePickerTable As Dictionary(Of INotifyCollectionChanged, DatePicker)
Private Shared Sub New()
DatePickerHelper.DatePickerTable = New Dictionary(Of INotifyCollectionChanged, DatePicker)()
End Sub
Private Shared Sub OnBlackDatesChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim attachedDatePicker = TryCast(d, DatePicker)
Dim oldObservableCollection As INotifyCollectionChanged = Nothing
If CSharpImpl.__Assign(oldObservableCollection, TryCast(e.OldValue, INotifyCollectionChanged)) IsNot Nothing Then
oldObservableCollection.CollectionChanged -= AddressOf UpdateDatePickerBlockedDates
DatePickerHelper.DatePickerTable.Remove(oldObservableCollection)
End If
Dim newObservableCollection As INotifyCollectionChanged = Nothing
If CSharpImpl.__Assign(newObservableCollection, TryCast(e.NewValue, INotifyCollectionChanged)) IsNot Nothing Then
newObservableCollection.CollectionChanged += AddressOf UpdateDatePickerBlockedDates
DatePickerHelper.DatePickerTable.Add(newObservableCollection, attachedDatePicker)
End If
attachedDatePicker.BlackoutDates.AddRange(TryCast(e.NewValue, IEnumerable(Of CalendarDateRange)))
End Sub
Private Shared Sub UpdateDatePickerBlockedDates(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
Dim attachedDatePicker As DatePicker = Nothing
If Not DatePickerHelper.DatePickerTable.TryGetValue(TryCast(sender, INotifyCollectionChanged), attachedDatePicker) Then
Return
End If
Select Case e.Action
Case NotifyCollectionChangedAction.Add
attachedDatePicker.BlackoutDates.AddRange(e.NewItems.OfType(Of CalendarDateRange)())
Case NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Replace
e.OldItems.OfType(Of CalendarDateRange)().ToList().ForEach(Function(removedItem) attachedDatePicker.BlackoutDates.Remove(removedItem))
Case NotifyCollectionChangedAction.Move
Case NotifyCollectionChangedAction.Reset
attachedDatePicker.BlackoutDates.Clear()
Case Else
End Select
End Sub
End Class
ViewModel.cs
Class ViewModel
Public Property BlockedDates As ObservableCollection(Of CalendarDateRange)
// Block Christmas holidays and all days in the past
Public Sub New()
Return CSharpImpl.__Assign(Me.BlockedDates, New ObservableCollection(Of CalendarDateRange) From {
New CalendarDateRange(New DateTime(2020, 12, 24), New DateTime(2020, 12, 26)),
New CalendarDateRange(DateTime.MinValue, DateTime.Today.Subtract(TimeSpan.FromDays(1)))
})
End Sub
End Class
MainWindow.xaml
<!-- Only show dates from today -->
<DatePicker DatePickerHelper.BlackedDays="{Binding BlockedDates}" />
I use for my WPF project a binding of a table (db) to a listview. But if i start my project, the listview is empty. Iam using linq to get the data of my entity-framework and address has definitly the right string in it.
Is my binding is wrong and how to fix it?
xaml
<ListView ItemsSource="{Binding Items}" x:Name="lstvw_Overview" >
<ListView.View>
<GridView>
<GridViewColumn Header="Adresse"
DisplayMemberBinding="{Binding structureAddress}"/>
</GridView>
</ListView.View>
</ListView>
code
Iam pretty sure that my table is filled
Public Sub New()
Initialize()
End Sub
Dim address As String
Public Structure Uebersicht
Dim structureAddress As String
Shared _items As ObservableCollection(Of Uebersicht) = New ObservableCollection(Of Uebersicht)
Public Property Items As ObservableCollection(Of Uebersicht)
Get
Return _items
End Get
Set(value As ObservableCollection(Of Uebersicht))
_items = value
End Set
End Property
End Structure
Sub Initialize()
InitializeComponent()
DataContext = Me
fillListView()
End Sub
Sub fillListView()
Using container As New infrastrukturDB_TESTEntities1
Dim mailAddressList = From tbl_unzustellbarAdressen In container.tbl_unzustellbarAdressen
For Each mail In mailAddressList
address = mail.unzustellbarMail.ToString()
Try
Uebersicht._items.Add(New Uebersicht With {.structureAddress = address})
Catch ex As Exception
MessageBox.Show("error")
End Try
Next
End Using
End Sub
For WPF bindings, always provide a Property definition, fields are not supported by the Binding class. So you need to turn structureAddress into a property in order to make it work.
Since you set your Window (?) class instance as its own DataContext and try to bind ItemsSource="{Binding Items}", your window class needs to contain a Property named Items with some collection type (eg ObservableCollection). So move your collection from within Structure Uebersicht to the outer Window class and don't use Shared on the backing field.
Note you don't really need the property setter on Items since you initialize _items once and then you only ever modify the contained items but not the collection reference itself.
by following this answer I coded almost same way. Below is part of my code.
XAML binding:
<cst:CustomDataGrid x:Name="grdItems" ItemsSource="{Binding myItems, IsAsync=True}" SelectedItemsList="{Binding SelectedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ViewModel:
Protected Overrides Sub DeleteCurrent()
Dim msg As New UserMessage()
If SelectedItems IsNot Nothing Then
For Each SelectedItem In SelectedItems
myItems.Remove(SelectedItem)
Next
RaisePropertyChanged("Items")
End If
End Sub
Private m_myItems As ObservableCollection(Of item)
Public Property myItems() As ObservableCollection(Of item)
Get
Return m_myItems
End Get
Set(value As ObservableCollection(Of item))
m_myItems = value
End Set
End Property
Private m_SelectedItem As IList = New List(Of item)()
Public Property SelectedItems() As IList
Get
Return m_SelectedItem
End Get
Set(value As IList)
m_SelectedItem = value
' RaisePropertyChanged("SelectedItems")
End Set
End Property
Selection works perfectly fine. But when I am looping to remove selected each item, I am getting exception that "Collection was modified. Enumeration operation may not execute.". For example, I select 3 rows and press delete keyboard button, when I observe my SelectedItems object, count is 3 as expected but right after executing the line with myItems.Remove(SelectedItem), count goes down to 1. why is that happening, i couldnt figure out because I am not modifing SelectedItems but myItems.
PS; customdatagrid is exactly same code as the original post above. thats why i didnt add it here.
" i couldnt figure out because I am not modifing SelectedItems but myItems"
You're removing item from myItems which used as ItemsSource of the data grid. Removing item from ItemsSource automatically followed by removing it from SelectedItems, and that behavior makes perfect sense (you can't keep an item selected when it is no longer in the data grid).
That's why the error. You can't let the collection changed while enumerating through it.
One possible way to accomplish this is by copying selected items to temporary collection, to a temporary array, for example. Then enumerate the array instead of the original collection :
Dim copyOfSelectedItems(SelectedItems.Count-1) As Object
SelectedItems.CopyTo(copyOfSelectedItems,0)
For Each SelectedItem In copyOfSelectedItems
myItems.Remove(SelectedItem)
Next
just copy selecteditems to new collection when doing foreach loop, e.g using ToArray linq extension method:
For Each SelectedItem In SelectedItems.ToArray()
myItems.Remove(SelectedItem)
Next
This is supposed to be easy, yet I can't get it to work.
This is a simplified example so I can illustrate my problem. I have a List(Of Object) and I want to bind the Content of a Label to some property of an object in the list. I want to do this in code (Because those labels will be generated in runtime).
I create my object that will hold the value for the label and the list, that will hold those objects:
' The List to hold objects
Public Class BList
Public Shared listy As New List(Of BindTest)
End Class
' Object to hold label text
Public Class BindTest
Public Property Namy As String
End Class
Then I try to create the object and add it to the list. And try to set the binding for the label (for sake of simplicity, lets say I want bind to the first list item).
Dim bb As New BindTest
bb.Namy = "FirstName"
BList.listy.Add(bb)
B_label.SetBinding(Label.ContentProperty, "Namy")
B_label.DataContext = BList.listy.Item(0)
So far it works fine and label shows "FirstName" as expected. But then if I try to change the value of that first item like this:
BList.listy.Item(0).Namy = "Something else"
nothing happens and the label is not updated.
Thanks to Mike Eason.
I needed to implement INotifyPropertyChanged. I somehow thought, that it would be implemented automatically with all that WPF default-way of databinding everything. Oh well, one needs implement this manually.
So for the code to work, this part:
' Object to hold label text
Public Class BindTest
Public Property Namy As String
End Class
..must be changed to this:
' Object to hold label text
Public Class BindTest
Implements INotifyPropertyChanged
Private _Namy As String
Public Property Namy
Set(value)
_Namy = value
_PropertyChanged("Namy")
End Set
Get
Return _Namy
End Get
End Property
Private Sub _PropertyChanged(Optional ByVal PropertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
Private Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
End Class
This might be a newbie question.
I want to show some information in a DataGrid. This does not have to be two-way binding, I only need to show the DataGrid as an output. The information is stored in a List that consists of List(Of String) items. The List(Of String) item being an entire row, where each String item within that list would go into its own column in the DataGrid.
So what would be the easiest way to do this?
I tried to do something like this:
Created the DataGrid:
<DataGrid x:Name="MyDataGrid" AutoGenerateColumns="True" ItemsSource="{Binding}" Margin="24,17,144,37">
</DataGrid>
Created the "row" entry by adding some data to the first list:
Dim MyEntryList As New List(Of String)
MyEntryList.Add("SomeName")
MyEntryList.Add("SomeInfo")
MyEntryList.Add("SomeStuff")
Added the created "row" list to the encompassing list:
Dim MyDataList As New List(Of List(Of String))
MyDataList.Add(MyEntryList)
And finally tried to set the source of my DataGrid:
Me.MyDataGrid.ItemsSource = MyDataList
Instead of those strings, the DataGrid shows some numbers describing Capacity and Count:
How can I get it to show the contents of the lists?
You should try to build some objects or Datatable to populate the Datagrid. Refer the code below
<DataGrid x:Name="MyDataGrid" AutoGenerateColumns="True" >
</DataGrid>
Imports System.Collections.ObjectModel
Class MainWindow
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
Dim lst As New ObservableCollection(Of SomeObject)
For index = 1 To 10
Dim obj As New SomeObject
obj.SomeName = "test" + index.ToString
obj.SomeInfo = "info" + index.ToString
lst.Add(obj)
Next
MyDataGrid.ItemsSource = lst
End Sub
End Class
Class SomeObject
Private name As String
Public Property SomeName() As String
Get
Return name
End Get
Set(ByVal value As String)
name = value
End Set
End Property
Private info As String
Public Property SomeInfo() As String
Get
Return info
End Get
Set(ByVal value As String)
info = value
End Set
End Property
End Class