How to bind Tooltip in code behind? - wpf

I have a class (which will serve as a base class for my user controls) that I want to be able to set the binding of the tooltip through the code behind. I can't seem to figure out quite how to make it work right. I want to set the binding to a property called "ToolTipText" by calling a function "SetToolTip" from the constructor.
Here is what I have so far:
Public MyBaseClass
Inherits UserControl
Private _ToolTipText As String = "This is the default text!!"
Public Property ToolTipText As String
Get
Return _ToolTipText
End Get
Set(value As String)
_ToolTipText = value
End Set
End Property
Private Sub SetToolTip()
Me.ToolTip = New ToolTip With {.MinHeight = 30, .MinWidth = 150, .FontSize = 16, .Foreground = Brushes.White}
Dim ToolTipBinding As Binding = New Binding
ToolTipBinding.Source = Me
ToolTipBinding.Path = New PropertyPath("ToolTipText")
ToolTipBinding.Mode = BindingMode.OneWay
ToolTipBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
BindingOperations.SetBinding(Me.ToolTip, ToolTipService.ToolTipProperty, ToolTipBinding)
End Sub
Public Sub New()
SetToolTip()
End Sub
End Class
However when I mouseover, I only get an empty tooltip (no text). I use snoop (if anyone else is familiar with that tool), and it doesn't list any bindings for my class' tooltip property. But the weird part is that if I delve into the tooltip property, I shows that the tooltip has it's own tooltip property which is set to the correct text. It seems I somehow need to bind to the tooltip's content and not it's own tooltip property.

So I just figured it out. The line which sets the binding should read:
BindingOperations.SetBinding(Me.ToolTip, ContentControl.ContentProperty, ToolTipBinding)
I's kind of strange to think, but before I was setting the tooltip's tooltip.

Related

How do I change the style of FindTextBox of FlowDocumentReader to match the theme of my app?

I have a WPF app with a FlowDocumentReader control and when I click the search button to search the text, the text box doesn't match the theme of the app. The text does, but the background of the text box doesn't. So if I am using a dark theme, the text I type into the text box is white (which is correct), but the text box background is also white (incorrect, making text not legible).
Does anyone know how to fix this? I tried applying a style but I don't know which component to target.
Wow, Microsoft really does not make this easy.
My Process
I tried the easy trick of adding a Style TargeType="TextBox" to FlowDocumentReader.Resources, but that doesn't work.
I tried doing things the "right" way and overriding FlowDocumentReader's ControlTemplate, but the TextBox in question isn't even part of the ControlTemaplte! Instead, there's a Border named PART_FindToolBarHost. The TextBox we want is added as a child to PART_FindToolBarHost in code- but only after the user has clicked the "find" button (the one with the magnifying glass icon). You can see this for yourself by looking at the control's source code.
With no more XAML-only ideas, I had to resort to using code. We need to somehow get a reference to the TextBox being created, and I can't think of any better way than to manually search the visual tree for it. This is complicated by the fact that the TextBox only exists once the find command has been executed.
Specifically, the find button in FlowDocumentReader binds to ApplicationCommands.Find. I tried adding a CommandBinding for that command to FlowDocumentReader so I could use it as a trigger to retrieve the TextBox. Unfortunately, adding such a CommandBinding somehow breaks the built-in functionality and prevents the TextBox from being generated at all. This breaks even if you set e.Handled = False.
Luckily, though, FlowDocumentReader exposes an OnFindCommand- except, of course, it's a Protected method. So I finally gave in and decided to inherit FlowDocumentReader. OnFindCommand works as a reliable trigger, but it turns out the TextBox isn't created until after the sub finishes. I was forced to use Dispatcher.BeginInvoke in order to schedule a method to run after the TextBox was actually added.
Using OnFindCommand as a trigger, I was finally able to reliably get a reference to the "find" TextBox, which is actually named FindTextBox.
Now that I can get a reference, we can apply our own Style. Except: FindTextBox already has a Style, so unless we want to override it, we're going to have to merge the two Styles. There's no publicly-accessible method for this (even though WPF does this internally in places), but luckily I already had some code for this.
The Working Code
First, a Module with the helper methods I used:
FindVisualChild is used to loop through the visual tree and get a reference to FindTextBox.
MergeStyles is used to combine the existing Style with the Style we supply, once we have that reference.
Module OtherMethods
<Extension()>
Public Function FindVisualChild(obj As DependencyObject, Name As String) As FrameworkElement
For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(obj) - 1
Dim ChildObj As DependencyObject = VisualTreeHelper.GetChild(obj, i)
If TypeOf ChildObj Is FrameworkElement AndAlso DirectCast(ChildObj, FrameworkElement).Name = Name Then Return ChildObj
ChildObj = FindVisualChild(ChildObj, Name)
If ChildObj IsNot Nothing Then Return ChildObj
Next
Return Nothing
End Function
Public Function MergeStyles(ByVal style1 As Style, ByVal style2 As Style) As Style
Dim R As New Style
If style1 Is Nothing Then Throw New ArgumentNullException("style1")
If style2 Is Nothing Then Throw New ArgumentNullException("style2")
If style2.BasedOn IsNot Nothing Then style1 = MergeStyles(style1, style2.BasedOn)
For Each currentSetter As SetterBase In style1.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style1.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style1.Resources.Keys
R.Resources(key) = style1.Resources(key)
Next
For Each currentSetter As SetterBase In style2.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style2.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style2.Resources.Keys
R.Resources(key) = style2.Resources(key)
Next
Return R
End Function
End Module
Then, there's StyleableFlowDocumentReader, which is what I named my extended control that inherits FlowDocumentReader:
Public Class StyleableFlowDocumentReader
Inherits FlowDocumentReader
Protected Overrides Sub OnFindCommand()
MyBase.OnFindCommand()
Dispatcher.BeginInvoke(Sub() GetFindTextBox(), DispatcherPriority.Render)
End Sub
Private Sub GetFindTextBox()
findTextBox = Me.FindVisualChild("FindTextBox")
ApplyFindTextBoxStyle()
End Sub
Private Sub ApplyFindTextBoxStyle()
If findTextBox IsNot Nothing Then
If findTextBox.Style IsNot Nothing AndAlso FindTextBoxStyle IsNot Nothing Then
findTextBox.Style = MergeStyles(findTextBox.Style, FindTextBoxStyle)
Else
findTextBox.Style = If(FindTextBoxStyle, findTextBox.Style)
End If
End If
End Sub
Private findTextBox As TextBox
Public Property FindTextBoxStyle As Style
Get
Return GetValue(FindTextBoxStyleProperty)
End Get
Set(ByVal value As Style)
SetValue(FindTextBoxStyleProperty, value)
End Set
End Property
Public Shared ReadOnly FindTextBoxStyleProperty As DependencyProperty =
DependencyProperty.Register("FindTextBoxStyle",
GetType(Style), GetType(StyleableFlowDocumentReader),
New PropertyMetadata(Nothing, Sub(d, e) DirectCast(d, StyleableFlowDocumentReader).ApplyFindTextBoxStyle()))
End Class
And then, finally, a usage example:
<local:StyleableFlowDocumentReader x:Name="Reader">
<local:StyleableFlowDocumentReader.FindTextBoxStyle>
<Style TargetType="TextBox">
<Setter Property="Foreground" Value="Blue"/>
</Style>
</local:StyleableFlowDocumentReader.FindTextBoxStyle>
<FlowDocument/>
</local:StyleableFlowDocumentReader>

WPF: Design time support for dependency properties with default values

I have written a custom control based on a ListBox. It contains a default ItemTemplate which shows an image given to the ListBox by a custom dependency property. The control also contains a default image, which is used when the user doesn't give an image to the dependency property.
This works so far, but now I've found a little problem and I don't know how to fix that.
When I use my custom control in the XAML designer, it first shows the default image. When I set the image's dependency property to an other image, the new image is immediately shown in the XAML designer.
But when I remove the XAML attribute for the new image again, the XAML designer only shows a white rectangle instead of the default image.
I assume it's because with setting the image's dependency property to some value and then removing it I nulled the value. But even when I check for null in the CoerceCallback and give back the default image when the coerced value is null, doesn't work.
What's the best way to support fallback values for dependency properties?
TestControl.vb
Public Class TestControl
Inherits ListBox
Private Shared _defaultResources As ResourceDictionary
Shared Sub New()
_defaultResources = New ResourceDictionary
_defaultResources.Source = New Uri("...")
End Sub
Public Shared ReadOnly TestProperty As DependencyProperty = DependencyProperty.Register(NameOf(TestControl.Test),
GetType(ImageSource),
GetType(TestControl),
New FrameworkPropertyMetadata(Nothing,
AddressOf TestControl.OnTestChanged,
AddressOf TestControl.OnTestCoerce))
Public Property Test As ImageSource
Get
Return DirectCast(MyBase.GetValue(TestControl.TestProperty), ImageSource)
End Get
Set(value As ImageSource)
MyBase.SetValue(TestControl.TestProperty, value)
End Set
End Property
Private Shared Sub OnTestChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
End Sub
Private Shared Function OnTestCoerce(d As DependencyObject, value As Object) As Object
If (value Is Nothing) Then
Return TryCast(_defaultResources.Item("TestImage"), ImageSource)
End If
Return value
End Function
Public Sub New()
Me.Test = TryCast(_defaultResources.Item("TestImage"), ImageSource)
End Sub
End Class
When I use that control like this
<local:TestControl ItemsSource="{Binding Items}" />
every item shows the default image at design time. When I change the XAML to
<local:TestControl ItemsSource="{Binding Items}"
Test="{StaticResource NewImage}" />
every item shows the new item at design time. But when I remove the Test="{StaticResource NewImage}" again, it doesn't go back to the default image.
Ok, after some testing (using this technique) I have discovered the source of your issue.
First of all, you are not using PropertyMetadata to set your default value, but instead the constructor. I assume you have a good reason to do so, but this essentially means that now you are relying on the coercion callback to set the default value.
However, it is not called (the framework assumes that your "true" default value - Nothing - doesn't need to be validated) after you remove the
Test="{StaticResource TestString}" line. Only the OnTestChanged
is called. This means we can use it to restore the default value:
void OnTestChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is null)
{
((TestControl)d).Test = yourDefaultImage;
return;
}
//Actual "OnChanged" code
}
A clumsy solution indeed, but it works. Depending on your exact situation, you might also want to take a look at Binding's FallbackValue and TargetNullValue properties:
Test="{Binding Source={ }, FallbackValue={ }, TargetNullValue={ }}"

WPF INotifyErrorInfo Validation.Error Event Not Raising

I'm encountering a strange problem. Despite setting everything correctly, the Validation.Error doesn't get fired.
Here are the details:
<DataTemplate x:Key="dtLateComers">
<TextBox Text="{Binding ParticipantTag, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True, NotifyOnSourceUpdated=True}" Validation.Error="Validation_Error" >
</DataTemplate>
Code behind (VB.Net) to set ItemsSource of HeaderedItemsControl:
hicLateComers.ItemsSource = _LateComersViewModels
_LateComersViewModels is ObservableCollection(Of ParticipantViewModel)
Implementation of ParticipantViewMode:
Public Class ParticipantViewModel
Implements INotifyPropertyChanged, IDataErrorInfo
Private _ParticipantTag As String = ""
Public Property ParticipantTag() As String
Get
Return _ParticipantTag
End Get
Set(ByVal value As String)
_ParticipantTag = value
_ParticipantTag= _ParticipantTag.ToUpper
NotifyPropertyChanged("ParticipantTag")
End Set
End Property
Public ReadOnly Property Item(byVal columnName As String) As String Implements IDataErrorInfo.Item
Get
Dim errorString As String = String.Empty
If columnName.Equals("ParticipantTag") Then
If not ParticipantValidationManager.IsValidKeypadTag(_ParticipantTag, True) then
errorString = "Incorrect entry. Please try again."
End If
End If
Return errorString
End Get
End Property
Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error
Get
Throw New NotImplementedException()
End Get
End Property
End Class
Problem
When I set ItemSource property (as mentioned above in code), Item index is called as many times as there are items in _LaterComersViewModels. Validation works and as a result I get red circle next to TextBox. However, Validation_Error never gets fired until I start typing in Textbox. Typing in TextBox changes the Property binds to it and validate it. Base on validation Validation.Error event is raised, and handled by application. Within that event handler I maintain a count of errors.
So the Question is, why Validation.Error doesn't get raised when one/more items fail on a validation rule during initial data binding? Though it does get raised once property is changed by typing into that TextBox.
Feel free to share any idea, assumption or a solution. Any type of help will be appreciated. Thanks.
Side note: I've a simple C# application which doesn't use data templating. In that application, Validation.Error event gets raised perfectly on start, and on property change. Though in that application, Model is binding to DataContext property of Grid.
Since Validation.Error is an attached event, you could hook up the event handler on the HeaderedItemsControl:
<HeaderedItemsControl x:Name="hicLateComers" ItemTemplate="{StaticResource dtLateComers}" Validation.Error="Validation_Error" />
The result should be pretty much the same since you can easily access both the TextBox and the ParticipantViewModel object in the event handler:
Private Sub Validation_Error(sender As Object, e As ValidationErrorEventArgs)
Dim textBox = CType(e.OriginalSource, TextBox)
Dim participant = CType(textBox.DataContext, ParticipantViewModel)
'...
End Sub

wpf - vb - animating gridsplitter / dock panel

this relates to my earlier question - I want to animate a grid splitter (to make panels slide into / out of view). We are pretty good at VB and already have a VB project, so would like to stay with VB if we can, but most WPF examples seem to be in XAML or CS.
I have some simple VB animation code working, BUT:
Of course, what needs to be animated is the width / height of the grid column / row, and this is not a dependency property. I found some clever stuff in CS to make a dependency property but could not translate this to vb. So I found a simple workaround which is to animate a dockpanel in the grid cell, catch it's size changed events and use these to set the cell grid size. It works but I wonder if it's less efficient as 2 things are being changed separately? Also I have to (when the animation completes) set the grid cells sizes back to * in the right proportion, and the dockpanel size back to auto.
It works, but it seems a bit clumsy - does someone have an example of making the animation for the grid work directly from VB any any other suggestions?
Thanks
For reference, here is the VB code for a dependency property to animate a gridsplitter:
Public Class GridLengthAnimation
Inherits AnimationTimeline
Public Sub New()
End Sub
Public Property From() As GridLength
Get
Return DirectCast(GetValue(FromProperty), GridLength)
End Get
Set(value As GridLength)
SetValue(FromProperty, value)
End Set
End Property
Public Shared ReadOnly FromProperty As DependencyProperty
= DependencyProperty.Register("From", GetType(GridLength),
GetType(GridLengthAnimation))
Public Property [To]() As GridLength
Get
Return DirectCast(GetValue(ToProperty), GridLength)
End Get
Set(value As GridLength)
SetValue(ToProperty, value)
End Set
End Property
Public Shared ReadOnly ToProperty As DependencyProperty
= DependencyProperty.Register("To", GetType(GridLength),
GetType(GridLengthAnimation))
Public Overrides ReadOnly Property TargetPropertyType() As Type
Get
Return GetType(GridLength)
End Get
End Property
Protected Overrides Function CreateInstanceCore() As Freezable
Return New GridLengthAnimation()
End Function
Public Overrides Function GetCurrentValue
(defaultOriginValue As Object,
defaultDestinationValue As Object,
animationClock As AnimationClock) As Object
Dim fromValue As Double = Me.From.Value
Dim toValue As Double = Me.[To].Value
If fromValue > toValue Then
Return New GridLength((1 - animationClock.CurrentProgress.Value)
* (fromValue - toValue) + toValue,
If(Me.[To].IsStar, GridUnitType.Star, GridUnitType.Pixel))
Else
Return New GridLength((animationClock.CurrentProgress.Value) *
(toValue - fromValue) + fromValue,
If(Me.[To].IsStar, GridUnitType.Star, GridUnitType.Pixel))
End If
End Function
End Class

WPF: Data binding with code

How do I use data-binding from code (C# or VB)?
This is what I have so far, but it is displaying Binding.ToString instead of m_Rep.FirstName.
Public ReadOnly Property TabCaption As Object
Get
Return New Label With {.Foreground = Brushes.Black, .Content = New Binding("FirstName"), .DataContext = m_Rep}
End Get
End Property
Yes, binding in code is a little different from straight assignment (which is how XAML makes it look like it works).
I can give you an example in C# - shouldn't be too far removed from VB.NET.
var label = new Label { Foreground = Brushes.Black, DataContext = m_Rep };
label.SetBinding(Label.ContentProperty, new Binding("FirstName"));
return label;
So the "SetBinding" method binds the "FirstName" path (of the DataContext) to the label's Content property.
You should use m_Rep as a Source of Binding
I have some sample C# code for you as below
Person myDataSource = new Person("Joe");
// Name is a property which you want to bind
Binding myBinding = new Binding("Name");
myBinding.Source = myDataSource;
// myText is an instance of TextBlock
myText.SetBinding(TextBlock.TextProperty, myBinding);
Hope to help

Resources