I am having trouble accessing a lable in my custom WPF control. I have implemented the PropertyChangedCallback but from there I can't get access to the class.
The class is a simple control with a lable (Name="lblDataName") that I want to change the text to match the DataName property.
Here is the code behind:
Public Class DataNameControl
Public Property DataName As String
Get
Return GetValue(DataNameProperty)
End Get
Set(ByVal value As String)
SetValue(DataNameProperty, value)
End Set
End Property
Public Shared ReadOnly DataNameProperty As DependencyProperty = _
DependencyProperty.Register("DataName", _
GetType(String), GetType(GraphData), _
New PropertyMetadata("KPI", AddressOf OnDataNameChanged))
Public Shared Function OnDataNameChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs) As String
Dim dnc As DataNameControl = CType(d, DataNameControl)
dnc.lblDataName.Text = e.NewValue.ToString
'' I want to access the lable on my class right here.
Return e.NewValue.ToString
End Function
End class
I would hope that setting the property in XAML would update the label accordingly, but nada!
<Controls:DataNameControl DataName="BCWP" Margin="0"/>
There are tons of answer on how to change the property itself, but none seem to answer this specifically, and if been tinkering for 20 hours on this one issue. Time ask for help!
The third parameter of the Register method is wrong. It must be GetType(DataNameControl) instead of GetType(GraphData):
Public Shared ReadOnly DataNameProperty As DependencyProperty = _
DependencyProperty.Register( _
"DataName", GetType(String), GetType(DataNameControl), _
New PropertyMetadata("KPI", AddressOf OnDataNameChanged))
The PropertyChangedCallback should not return a value:
Public Shared Sub OnDataNameChanged( _
ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim dnc As DataNameControl = CType(d, DataNameControl)
...
End Sub
Newbies (like me),
If you are reading this looking for a solution to your problem, the primary lesson here is, "make sure you are passing the proper parameters to your property registration." the VS widget will sort of help you through the process but only the first time, after that, if you change the name of your class, VS won't automagically change the class in the registration - that was the source of my problem.
SO a generic example based on the widget code added by VS2013 :
Public Shared ReadOnly Prop1Property As DependencyProperty = _
DependencyProperty.Register("Prop1", _
GetType(String), GetType(Window1), _
New PropertyMetadata(Nothing))
Prop1Property is your property name plus the suffix property.
"Prop1" is that property name.
1st GetType(String) is the variable type is the type of YOUR property. VS enters string as a default.
2nd GetType(Window1) is simply the class in which your property exists. VS enters "Window1" as a default. This is likely never correct and its not linked to anything else in the widget so you will have to change it yourself. I say simply, but this is where I messed up.
New PropertyMetadata has 6 overloads depending on how you planning on handling the values of the property.
In this case, "Nothing" explicitly states the default value is nothing.
Options include Default values, Property change callbacks, CoerceValueCallback and an IsValidValueCallback. All of that is a little beyond, my problem. I have to sort of figure them out to get my code working, so if anybody has questions, reply and I might add some info.
If you are new to Dependency Properties, which the two 10-minute videos.
Youtube: WPF Tutorial 21 - Dependency Properties
They are simple and enough to get you up and running.
Related
I am trying to achieve the following in a WPF personal finance app:
In various places I want to display a user control giving details of a asset holding (usually a share, bond etc), the target asset may be changed dynamically by the user in which case the control must be refreshed. Each Asset has a unique identifier, AssetId.
I am using MVVM and I've developed a single window with a View Model that takes AssetID as a parameter (property) and retrieves the relevant details for binding to the View. This work fine. What I'd like to do is make a generic user control with the same functionality so I can basically drop that 'window' inside other windows.
So I pretty much copy-pasted the XAML from that form into a User Control, where I'm struggling is passing in the AssetId from the parent window to the child control.
Google tells me I need a dependency property and here's where I am
Public Class HoldingView
Private _AssetId As Integer
Public AssetIdProperty As DependencyProperty = DependencyProperty.Register("AssetId",
GetType(Integer),
GetType(HoldingView),
New FrameworkPropertyMetadata(New PropertyChangedCallback(AddressOf AssetIDChanged)))
Public Property AssetId As Integer
Get
Return GetValue(AssetIdProperty)
End Get
Set(value As Integer)
SetValue(AssetIdProperty, value)
End Set
End Property
Private Sub AssetIDChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim NewAssetId As Integer
NewAssetId = e.NewValue
Me.DataContext.AssetId = NewAssetId
End Sub
Public Sub New()
' This call is required by the designer.
InitializeComponent()
Me.DataContext = New HoldingViewmodel
End Sub
End Class
Called like this:
<Grid>
<local:HoldingView AssetId="{Binding AssetId}"/>
</Grid>
The code compiles and runs but when I try and load the window that has the user control, the app crashes with this message:
A 'Binding' cannot be set on the 'AssetId' property of type 'HoldingView'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Which is not that helpful. From my Googling, you can also get this message if the syntax of the DP registration is not spot on, but it looks Ok to my inexperienced eye...
Anybody else had this?
Public AssetIdProperty As DependencyProperty
should be
Public Shared ReadOnly AssetIdProperty As DependencyProperty
Please take a look at Custom Dependency Properties.
Also remove
Me.DataContext = New HoldingViewmodel
because that will effectively break any DataContext-based Bindings like
AssetId="{Binding AssetId}"
where the source property is supposed to be owned by the object in the inherited DataContext, which usually is an object in the application's view model.
Controls should never have their own, "private" view model, but instead handle property changes in code behind. In case of UserControls, there could simply be UI elements in their XAML that would be bound to the UserConrol's own properties.
Hence
Me.DataContext.AssetId = NewAssetId
in the PropertyChangedCallback is pointless and should be removed, as well as
Private _AssetId As Integer
To summarize, it should look like this:
Public Class HoldingView
Public Shared ReadOnly AssetIdProperty As DependencyProperty =
DependencyProperty.Register(
"AssetId",
GetType(Integer),
GetType(HoldingView),
New FrameworkPropertyMetadata(
New PropertyChangedCallback(AddressOf AssetIdPropertyChanged)))
Public Property AssetId As Integer
Get
Return GetValue(AssetIdProperty)
End Get
Set(value As Integer)
SetValue(AssetIdProperty, value)
End Set
End Property
Private Shared Sub AssetIdPropertyChanged(
d As DependencyObject, e As DependencyPropertyChangedEventArgs)
CType(d, HoldingView).AssetIdChanged(e.NewValue)
End Sub
Private Sub AssetIdChanged(id As Integer)
...
End Sub
Public Sub New()
InitializeComponent()
End Sub
End Class
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
I have created an dependency property as a List(Of String) in my custom component:
Public Shared ReadOnly ErrorCharactersProperty As DependencyProperty = _
DependencyProperty.RegisterAttached("ErrorCharacters", _
GetType(List(Of String)), _
GetType(MaterialDesign.TitledTextBox), _
New UIPropertyMetadata(New List(Of String)))
And the property for it:
Public Property ErrorCharacters As List(Of String)
Get
Return CType(MyBase.GetValue(ErrorCharactersProperty), List(Of String))
End Get
Set(value As List(Of String))
MyBase.SetValue(ErrorCharactersProperty, value)
End Set
End Property
So it should work as a charm and as my other DependencyProperties. The problem is that when I call it and fill my ErrorCharacters with some strings:
<MaterialDesign:TitledTextBox HorizontalAlignment="Left" Margin="246,184,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="187" TitleText="E-Mail">
<MaterialDesign:TitledTextBox.ErrorCharacters>
<System:String>#</System:String>
</MaterialDesign:TitledTextBox.ErrorCharacters>
</MaterialDesign:TitledTextBox>
TitleText is another DependencyProperty that works as it should.
When I call it like this it copies the list to all my other custom components of the same type. I know it cause of controls that I made inside that control. However it copies that List and gives the same result to all other same component of the same type where it shouldn't do that.
Notice that you are passing an actual instance of the list as property default value (which is stored statically), and not a delegate which would provide default value on class instantiation. Hence, if not set explicitly, all instances share the same instance of the list as the value of the property. That being said, if you add an item to the list, it is available/visible to all instances of that type. To resolve this issue you need to set the default property value in the constructor, and not through the property matadata.
I'm trying to create a UserControl which consists of a DataGrid and a couple of buttons. The buttons will handle adding/deleting rows (needs to be buttons). The DataGrid is bound to a custom observable collection. The collections properties will vary (so I'm auto-generating the columns).
How can I add a new row? Normally I'd just modify the observable collection. I've tried adding a new row directly to the control:
dgMain.Items.Add(New DataGridRow())
but I get an error which doesn't mean much to me:
Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead.
Here's the current code behind:
Public Class DataGrid
Sub New()
InitializeComponent()
End Sub
#Region "Dependency Properties"
Public Shared MyItemsSourceProperty As DependencyProperty = DependencyProperty.Register("MyItemsSource", GetType(IEnumerable), GetType(DataGrid))
Public Property MyItemsSource() As IEnumerable
Get
Return DirectCast(GetValue(MyItemsSourceProperty), IEnumerable)
End Get
Set(value As IEnumerable)
SetValue(MyItemsSourceProperty, value)
End Set
End Property
#End Region
#Region "Buttons"
Private Sub btnAdd_Click(sender As System.Object, e As System.Windows.RoutedEventArgs) Handles btnAdd.Click
dgMain.Items.Add(New DataGridRow())
End Sub
#End Region
End Class
So does anyone know how I can add a new row?
Thanks for any help.
EDIT: This is how the data is created:
Dim np As New ObPerson
np.Add(New Person With {.FirstName = "Jane", .LastName = "Mitel", .Age = 18})
np.Add(New Person With {.FirstName = "Joe", .LastName = "Bloggs", .Age = 92})
UserControlInstance.MyItemsSource = np
Public Class ObPerson
Inherits ObservableCollection(Of Person)
End Class
EDIT2: VB Version of the accepted answer:
Public Shared Sub AddNewElement(l As IList)
If l Is Nothing OrElse l.Count = 0 Then
Throw New ArgumentNullException()
End If
Dim obj As Object = Activator.CreateInstance(l(0).[GetType]())
l.Add(obj)
End Sub
Usage: AddNewElement(MyItemsSource)
You need to use the collection that's bound - not the 'Items' property on the grid. ItemsSource will point to your collection that is bound:
SomeGrid.ItemsSource = SomeCollection;
SomeCollection.Add(new ItemOfTheRightType());
or
(SomeGrid.ItemsSource as SomeCollection).Add(new ItemOfTheRightType());
The error says that you can't use Grid.Items if you are binding using Grid.ItemsSource
Edit:
If you don't know the item type at runtime (maybe because this is a 3rd party using the control etc and you want a generic add method) you need to call the .Add method on the underlying interface. Most list types inherit from IList in the .NET framework
I'm no VB expert, I much prefer c# so I'll give you the c#. You need to check for the underlying type first:
in c#
if(grid.ItemsSource is IList)
{
(grid.ItemsSource as IList).Add(new childType()); <-- other issue here..
}
The problem you have though is that if you are adding a new item to the collection and you don't know the list type, IList requires an instance of the object to add to the list
solution is to use reflection:
Dynamically creating a new instance of IList's type
An interesting late answer is:
var collectionType = targetList.GetType().GetProperty("Item").PropertyType;
var constructor = collectionType.GetConstructor(Type.EmptyTypes);
var newInstance = constructor.Invoke(null);
Which might work
I have a custom control that has a property that is an ObservableCollection of another custom control. I can't seem to get the DependencyProperty change event to fire in design time. I have tried to use CoerceValueCallback this doesn't fire either. can anyone give me some direction. Every thing else is working just fine in runtime i just can't get this to fire so i can update the control in designtime. Many thanks in advance.
Public Shared ReadOnly ArcsProperty As DependencyProperty = DependencyProperty.Register("Arcs", GetType(ObservableCollection(Of OPCWPF.OPCWPFArcControl)), GetType(OPCWPFPie), New PropertyMetadata(New ObservableCollection(Of OPCWPF.OPCWPFArcControl), New PropertyChangedCallback(AddressOf ArcsPropertyChanged), New CoerceValueCallback(AddressOf CoerceArcs)))
' Arc Quantity
<Description("Collection of Arcs"), _
Category("Common Properties")> _
Public Property Arcs() As ObservableCollection(Of OPCWPF.OPCWPFArcControl)
Get
Return DirectCast(Me.GetValue(ArcsProperty), ObservableCollection(Of OPCWPF.OPCWPFArcControl))
End Get
Set(ByVal value As ObservableCollection(Of OPCWPF.OPCWPFArcControl))
Me.SetValue(ArcsProperty, value)
End Set
End Property
Via the event you register with the dependency property metadata you are just subscribing change on the property itself, not changes to the collection (adding/removing items) that need to be subscribed by registering for the event CollectionChanged exposed by ObservableCollection<T>
The simple solution (maybe not the best) but it got me what i was looking for.
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
AddHandler Arcs.CollectionChanged, AddressOf UpdateControl
End Sub