Trying to register DependencyProperty for WebBrowser Control - wpf

I'm trying to get the WebBrowser control to display HTML loaded from a variable. I found this solution Displaying html from string in WPF WebBrowser control so am trying to implement my first DependencyProperty.
Here's my attempt at the implementation:
Public Shared ReadOnly HtmlProperty As DependencyProperty = DependencyProperty.RegisterAttached("Html", GetType(String), GetType(PortraitSingle), New FrameworkPropertyMetadata(OnHtmlChanged))
<AttachedPropertyBrowsableForType(GetType(WebBrowser))> _
Public Shared Function GetHtml(ByVal d As WebBrowser) As String
Return DirectCast(d.GetValue(HtmlProperty), String)
End Function
Public Shared Sub SetHtml(ByVal d As WebBrowser, ByVal value As String)
d.SetValue(HtmlProperty, value)
End Sub
Private Shared Sub OnHtmlChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim wb As WebBrowser = TryCast(d, WebBrowser)
If wb IsNot Nothing Then
wb.NavigateToString(TryCast(e.NewValue, String))
End If
End Sub
and the XAML
<WebBrowser x:Name="HTMLDescription" Grid.RowSpan="2" Margin="10" local:PortraitSingle.Html="{Binding HtmlToDisplay}"></WebBrowser>
First off, the problem I'm having is I'm getting the following errors:
Argument not specified for parameter 'e' of 'Private Shared Sub OnHtmlChanged(d As System.Windows.DependencyObject, e As System.Windows.DependencyPropertyChangedEventArgs)
and
The attachable property 'Html' was not found in type 'PortraitSingle'. (PortraitSingle is the name of my Window).
I can't work out how to get around them.
Also, I can't understand how I actually load my HTML into the WebBrowser control.
Ben

Found my problem.
Needed to add the 'AddressOf' to the DependencyProperty statement
Public Shared ReadOnly HtmlProperty As DependencyProperty = DependencyProperty.RegisterAttached("Html", GetType(String), GetType(PortraitSingle), New FrameworkPropertyMetadata(AddressOf OnHtmlChanged))

Related

WPF Dependency Property - set from XAML

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

Tracking the CurrentChanged event of an ICollectionView declared as a dependencyProperty

I have an ICollectionView declared like so;
Public Shared ReadOnly DataIcvProperty As DependencyProperty = DependencyProperty.Register("DataIcv", GetType(ICollectionView), GetType(DataNavigator), New PropertyMetadata(Nothing))
<Description("The CollectionView (as an ICollectionView) to be passed to the DataNavigator control"), Category("Navigation Data Source")>
Public Property DataIcv As ICollectionView
Get
Return GetValue(DataIcvProperty)
End Get
Set(ByVal Value As ICollectionView)
SetValue(DataIcvProperty, Value)
End Set
End Property
On which I would like to keep track of the CurrentChanged event. So in the constructor of a wpf userControl (in which this property is declared I have added the following:
AddHandler DataIcv.CurrentChanged, AddressOf OnDataICVCurrentChanged
and then added the following
Public Sub OnDataICVCurrentChanged(ByVal sender As Object, e As EventArgs) Handles internalIcv.CurrentChanged
'Do whatever needs doing when the record in the ICollectionView changes
End Sub
When I then compile the usercontrol and use it in a separate project I get a NullReferenceException on the AddHandler line in the Constructor of the control (without any inner exception details).
What do I need to do to keep track of the current Changed event of the DataIcv so that I can have elements of my user control react properly to those changes?
Thanks
EDIT
This relates to the issue I described here. Essentially I want the navigator control to reflect the fact that the end user might select rows on the grid independently of the navigator.
The problem is, when you try to subscribe to the CurrentChanged event in your constructor, the DependencyProperty DataIcvProperty has a default value Nothing (as you specified in your property registration). So the NullReferenceException.
You could solve this with a PropertyChangeCallback (see documentation).
I come from the C# world and cannot guarantee the right syntax for VB.NET, but this approach surely will work:
Public Shared ReadOnly DataIcvProperty As DependencyProperty =
DependencyProperty.Register("DataIcv",
GetType(ICollectionView), GetType(DataNavigator),
New FrameworkPropertyMetadata(
Nothing,
New PropertyChangedCallback(AddressOf OnDataIcvChanged)))
Then you need to implement the OnDataIcvChanged static method, which will be called every time the property value changes.
Private Shared Sub OnDataIcvChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
If e.OldValue IsNot Nothing
RemoveHandler (e.OldValue As ICollectionView).CurrentChanged, AddressOf OnDataICVCurrentChanged
EndIf
If e.NewValue IsNot Nothing
AddHandler (e.NewValue As ICollectionView).CurrentChanged, AddressOf OnDataICVCurrentChanged
EndIf
End Sub
Update:
If your event handler method is not static, then you should access it via the object instance:
Private Shared Sub OnDataIcvChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim DataNavigator instance = d As DataNavigator
If e.OldValue IsNot Nothing
RemoveHandler (e.OldValue As ICollectionView).CurrentChanged, AddressOf instance.OnDataICVCurrentChanged
EndIf
If e.NewValue IsNot Nothing
AddHandler (e.NewValue As ICollectionView).CurrentChanged, AddressOf instance.OnDataICVCurrentChanged
EndIf
End Sub
Edit: Code That eventually worked:
Public Shared ReadOnly DataIcvProperty As DependencyProperty = DependencyProperty.Register("DataIcv", GetType(ICollectionView), GetType(DataNavigator), New FrameworkPropertyMetadata(Nothing, New PropertyChangedCallback(AddressOf OnDataIcvChanged)))
<Description("The CollectionView (as an ICollectionView) to be passed to the DataNavigator control"), Category("Navigation Data Source")>
Public Property DataIcv As ICollectionView
Get
Return CType(GetValue(DataIcvProperty), ICollectionView)
End Get
Set(ByVal Value As ICollectionView)
SetValue(DataIcvProperty, Value)
End Set
End Property
Private Shared Sub OnDataIcvChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim dn As DataNavigator = CType(d, DataNavigator)
If e.OldValue IsNot Nothing Then
RemoveHandler dn.DataIcv.CurrentChanged, AddressOf dn.OnDataICVCurrentChanged
End If
If e.NewValue IsNot Nothing Then
AddHandler dn.DataIcv.CurrentChanged, AddressOf dn.OnDataICVCurrentChanged
End If
End Sub
Private Sub OnDataICVCurrentChanged(ByVal sender As Object, ByVal e As EventArgs)
Record.Text = (DataIcv.CurrentPosition + 1).ToString
End Sub

Error: " 'Subjects' property was already registered by 'Period' " is raised when more than one control is placed on the form

As you can see in the code below I have created a custom control named Period that inherits from Listbox. In it I have declared a read-only dependancy property named 'Subjects'. When a single Period is placed on the WPF window everything runs fine. However, when I place more than one I get the error mentioned in the title.
Here is the Period Class:
Public Class Period
Inherits System.Windows.Controls.ListBox
'-------- PROPERTIES --------'
Public ReadOnly Property Subjects() As ObservableCollection(Of Subject)
Get
Return Me.GetValue(SubjectsProperty)
End Get
End Property
Private ReadOnly SubjectsPropertyKey As DependencyPropertyKey = DependencyProperty.RegisterReadOnly("Subjects", GetType(ObservableCollection(Of Subject)), GetType(Period), New FrameworkPropertyMetadata(New ObservableCollection(Of Subject)))
Public ReadOnly SubjectsProperty As DependencyProperty = SubjectsPropertyKey.DependencyProperty
'-------- SUBROUTINES ---------'
Shared Sub New()
'This OverrideMetadata call tells the system that this element wants to provide a style that is different than its base class.
'This style is defined in themes\generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(GetType(Period), New FrameworkPropertyMetadata(GetType(Period)))
End Sub
Public Sub New()
MyBase.New()
Me.SetValue(SubjectsPropertyKey, New ObservableCollection(Of Subject))
End Sub
'-------- METHODS ---------'
Public Sub AddSubject(ByRef subject As Subject)
If Me.CheckForDuplicates(subject) = True Then
MsgBox("This subject is already present in this period.")
Else
Dim SubjectsList As New ObservableCollection(Of Subject)
SubjectsList = Me.GetValue(SubjectsProperty)
SubjectsList.Add(subject)
Me.SetValue(SubjectsPropertyKey, SubjectsList)
End If
End Sub
Public Sub RemoveSubject(ByRef subject As Subject)
If Me.CheckForDuplicates(subject) = False Then
MsgBox("This subject is not present in this period.")
Else
Dim SubjectsList As New ObservableCollection(Of Subject)
SubjectsList = Me.GetValue(SubjectsProperty)
SubjectsList.Remove(subject)
Me.SetValue(SubjectsPropertyKey, SubjectsList)
End If
End Sub
Public Function CheckForDuplicates(ByRef subject As Subject) As Boolean
Dim Conflict As Boolean
If Subjects.Contains(subject) Then
Conflict = True
End If
Return Conflict
End Function
Private Sub Period_Initialized(sender As Object, e As EventArgs) Handles Me.Initialized
Me.ItemsSource = Subjects
End Sub
End Class
Here is the code for the window:
<Grid Background="#FF2B2B2B">
<local:Period HorizontalAlignment="Left" VerticalAlignment="Top"/>
<local:Period HorizontalAlignment="Left" VerticalAlignment="Top"/>
</Grid>
SubjectsPropertyKey and SubjectsProperty has to be Shared. Otherwise it tries to register Subject with each instantiation of Period class and this fails after a successful registering.
See the Examples section in MSDN.

Periodically update silverlight view with MVVM

I am trying to use MVVM in Silverlight, but I am quite new to it so I am not quite sure on some things. I have a silverlight page which displays the progress of some server side operation. The current progress comes from a web service and should be refreshed every few seconds (lets say 10 seconds for the sake of the argument).
What is the best way to implement this? The options I could think of was:
Initalize a DispatcherTimer in the Initalize method of my ViewModel and refresh the view from the DispatcherTimer event (putting the timer details in the ViewModel)
Create a wrapper arround DispatcherTimer (e.g. PeriodicCommandExecutor) which would be a Control or resource similar to the Timer control in WindowsForms with a command property that I bind to a Refresh command in the ViewModel (putting the timer details in the View)
I think the second option is preferred, because it makes the ViewModel easier to test and DispatcherTimer is an UI implementation detail which I don't want in my ViewModel propably. Do you agree?
If yes, how would you create such a wrapper. I started doing an DependencyObject with attached properties, but I am not sure how to forward the property values like Interval to the internal DispatcherTimer. Silverlight doesn't seem to provide any events when the dependency properties change and DispatcherTimer is not a DependencyObject so I can't databind directly to its properties.
Thanks!
Why use a DispatcherTimer? Why not use an ordinary System.Threading.Timer, which will fire its callback on a background thread?
If you put your UI progress update somewhere inconspicious (i.e. not in the centre of the UI, maybe in a bottom corner or status bar), then have the background timer chugging away while the user carries on with what they were doing. The progress value can be populated into the viewmodel, and shown on the UI using binding. This way you don't have to tie up the UI thread making web service calls.
At the end I solved my dillema creating a behavior which periodically executes a refresh command on the ViewModel which you can specify.
The code for the behavior is like this
(sorry for VB code):
Option Strict On
Imports System.Windows.Threading
Imports System.Windows.Interactivity
Namespace View.Behaviors
Public Class RefreshBehavior
Inherits Behavior(Of FrameworkElement)
Public Property Command As ICommand
Get
Return DirectCast(GetValue(CommandProperty), ICommand)
End Get
Set(ByVal value As ICommand)
SetValue(CommandProperty, value)
End Set
End Property
Public Shared ReadOnly CommandProperty As DependencyProperty = _
DependencyProperty.Register("Command", _
GetType(ICommand), GetType(RefreshBehavior), _
New PropertyMetadata(Nothing))
Public Property CommandParameter As Object
Get
Return GetValue(CommandParameterProperty)
End Get
Set(ByVal value As Object)
SetValue(CommandParameterProperty, value)
End Set
End Property
Public Shared ReadOnly CommandParameterProperty As DependencyProperty = _
DependencyProperty.Register("CommandParameter", _
GetType(Object), GetType(RefreshBehavior), _
New PropertyMetadata(Nothing))
Public Property Interval As TimeSpan
Get
Return DirectCast(GetValue(IntervalProperty), TimeSpan)
End Get
Set(ByVal value As TimeSpan)
SetValue(IntervalProperty, value)
End Set
End Property
Public Shared ReadOnly IntervalProperty As DependencyProperty = _
DependencyProperty.Register("Interval", _
GetType(TimeSpan), GetType(RefreshBehavior), _
New PropertyMetadata(TimeSpan.Zero, AddressOf OnIntervalUpdate))
Public Property Enabled As Boolean
Get
Return DirectCast(GetValue(EnabledProperty), Boolean)
End Get
Set(ByVal value As Boolean)
SetValue(EnabledProperty, value)
End Set
End Property
Public Shared ReadOnly EnabledProperty As DependencyProperty = _
DependencyProperty.Register("Enabled", _
GetType(Boolean), GetType(RefreshBehavior), _
New PropertyMetadata(False, AddressOf OnEnabledUpdate))
Dim WithEvents timer As New DispatcherTimer()
Private Shared Sub OnEnabledUpdate(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim enable As Boolean = CType(e.NewValue, Boolean)
Dim executor As RefreshBehavior = CType(d, RefreshBehavior)
If Not executor.attached Then Return
Dim timer As DispatcherTimer = executor.timer
If enable AndAlso Not timer.IsEnabled Then
timer.Start()
ElseIf Not enable AndAlso Not timer.IsEnabled Then
timer.Stop()
End If
End Sub
Private Shared Sub OnIntervalUpdate(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim executor As RefreshBehavior = CType(d, RefreshBehavior)
Dim timer As DispatcherTimer = executor.timer
timer.Interval = CType(e.NewValue, TimeSpan)
End Sub
Private WithEvents attachedObject As FrameworkElement
Private Sub OnUnload(ByVal sender As Object, ByVal e As EventArgs) Handles attachedObject.Unloaded
timer.Stop()
End Sub
Private attached As Boolean = False
Protected Overrides Sub OnAttached()
attached = True
attachedObject = AssociatedObject
If Enabled Then timer.Start()
MyBase.OnAttached()
End Sub
Protected Overrides Sub OnDetaching()
timer.Stop()
attached = False
attachedObject = Nothing
MyBase.OnDetaching()
End Sub
Private Sub OnTick(ByVal sender As Object, ByVal e As EventArgs) Handles Timer.Tick
Dim cmd = Command
Dim parameter = CommandParameter
If Interval < TimeSpan.MaxValue AndAlso cmd IsNot Nothing AndAlso cmd.CanExecute(parameter) Then
cmd.Execute(parameter)
End If
End Sub
End Class
End Namespace
You can use it like this:
<i:Interaction.Behaviors>
<Behaviors:RefreshBehavior Enabled="True" Interval="0:0:10" Command="{Binding RefreshPageCommand}" />
</i:Interaction.Behaviors>
I hope it helps someone with a similar problem.

Attaching a custom DependencyProperty to a StackPanel in WPF?

I'm trying to make a mouse over for a stack panel in WPF using a custom DependencyProperty (StackPanels do not handle the MouseEnter event).
I've created a class for the DependencyProperty like so:
Public Class MouseEnterBehavior
Public Shared Property MouseEnterProperty As DependencyProperty =
DependencyProperty.RegisterAttached("MouseEnter",
GetType(ICommand),
GetType(MouseEnterBehavior),
New PropertyMetadata(Nothing, AddressOf MouseEnterChanged))
Public Shared Function GetMouseEnter(ByVal obj As DependencyObject) As ICommand
Return CType(obj.GetValue(MouseEnterProperty), ICommand)
End Function
Public Shared Sub SetMouseEnter(ByVal obj As DependencyObject, ByVal value As ICommand)
obj.SetValue(MouseEnterProperty, value)
End Sub
Public Shared Sub MouseEnterChanged(ByVal obj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim element As UIElement = TryCast(obj, UIElement)
If element IsNot Nothing Then
AddHandler element.MouseEnter, AddressOf uiElement_MouseEnter
End If
End Sub
Public Shared Sub uiElement_MouseEnter(ByVal sender As Object, ByVal e As EventArgs)
Dim uiElement As UIElement = TryCast(sender, UIElement)
Dim command As ICommand = GetMouseEnter(uiElement)
If command IsNot Nothing And command.CanExecute(uiElement) Then
command.Execute(uiElement)
End If
End Sub
End Class
My View looks like this:
<Window x:Class="MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Height="76" vm:MouseEnterBehavior.MouseEnterCommand="{Binding MouseEnteredCommand}" HorizontalAlignment="Left" Margin="212,117,0,0" VerticalAlignment="Top" Width="88" Background="#72000000" />
</Grid>
</Window>
And my ViewModel looks like this:
Public Class MainWindowViewModel
Inherits ViewModelBase
Implements INotifyPropertyChanged
Private cmdMouseCommand As RelayCommand
Public ReadOnly Property MouseEnteredCommand As ICommand
Get
If cmdMouseCommand Is Nothing Then
cmdMouseCommand = New RelayCommand(AddressOf OnMouseEnterCommand)
End If
Return cmdMouseCommand
End Get
End Property
Private Sub OnMouseEnterCommand(ByVal obj As Object)
''//Do something
End Sub
End Class
Update
I was able to get the code to compile and run, however the binding doesn't occur. I can't seem to figure out why.
You've registered the dependency property as MouseEnteredCommand, but try to bind to MouseEnterCommand.
On a side note, the binding won't set your DependencyProperty using the Set call you've provided; it will call SetValue directly. You'll need to pass a callback into the RegisterAttached so that you'll be notified.
I do believe that this is your problem:
DependencyProperty.RegisterAttached("MouseEnteredCommand",
GetType(ICommand),
GetType(MainWindowViewModel)
the first GetType should be the type of the property ( you are OK here )
the second GetType shuold be the type of the containing class, in your case "MouseEnterBehavior"

Resources