Introduction
In this article I will show you examples of three implementations using attached properties, that quite simply lets you add convenient behavior to an existing control (or class) with one line of XAML code, instead of the alternative, to inherit the control in a custom class to just expand it with a property. The usage of attached properties is quite widespread even if you have never heard of them. The most common example I can think of would be Grid.Row and Grid.Column properties. And they are examples of how attached behaviors should work, a reusable code that takes a minimal of time to set up, and solves a reoccurring problem in quite an elegant fashion.
Background
An attached property is quite similar to an Extension of an existing class, exemplified below by the function Reverse
that can be called on every String
in the application now:
Module Extensions
<System.Runtime.CompilerServices.Extension>
Public Function Reverse(ByVal OriginalString As String) As String
Dim Result As New Text.StringBuilder
Dim chars As Char() = OriginalString.ToCharArray
For i As Integer = chars.Count - 1 To 0 Step -1
Result.Append(chars(i))
Next
Return Result.ToString
End Function
End Module
While this is valid everywhere, the attached property is far more useful, as one can decide which instance of the class you wish to extend. They are also DependencyProperties, which supports binding to other elements as well.
To create an attached dependency property, you create a new class (called MyNewClass
here), and declare it like this:
Public Shared ReadOnly SearchTextProperty As DependencyProperty =
DependencyProperty.RegisterAttached("SearchText",
GetType(String),
GetType(MyNewClass),
New FrameworkPropertyMetadata(
Nothing,
FrameworkPropertyMetadataOptions.AffectsRender,
New PropertyChangedCallback(AddressOf OnSearchTextChanged)))
The difference between a normal dependency property and an attached is this call with an attached property:
DependencyProperty.RegisterAttached
and below is valid for a normal dependency property
DependencyProperty.Register
The attached property is so useful that an abstraction layer have been created to encapsulate it in Blend, called Behavior, and you could read more about it on Brian Noyes blog. To get a glimce of how you can create one yourself, you can check out Jason Kemp's blog post. His code lets you add elements in a way that is similar to the Behavior class, and a VB.NET version is added to the VS2013 project under the folder called Unused.
To use it within the project you can type in the following:
<TreeView>
<local:Behavior.ContentPresenter>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Height="23">
<Run Text="Show some text" FontSize="18"/>
</TextBlock>
</StackPanel>
</local:Behavior.ContentPresenter>
....
</TreeView>
Binding Enum to ComboBox
Assuming that you want to bind an Enum
to a ComboBox
in order to show the current value and allow you to change it with the help of changing the SelectedItem
in the ComboBox
. This problem seems to be a reoccurring theme with several different solutions posted, even some with attached behavior:
I will assume that you want to just show the Enum
in a ComboBox
with a human readable string, and I will also assume that we all know this trick with adding a readable description to the Enum. I will go through it just for the sake of completeness, and you simply do this:
Public Enum TheComboBoxShow
<System.ComponentModel.DescriptionAttribute("The show is on")>
[On]
<System.ComponentModel.DescriptionAttribute("The show is off")>
[Off]
End Enum
Now, the Description attribute can be collected from the Enum
property by the use of a helper function. This particular one I stole from OriginalGriff, but many other have also created similar functions based on the MSDN example:
Public Shared Function GetDescription(value As [Enum]) As String
Dim fi As FieldInfo = value.[GetType]().GetField(value.ToString())
Dim attributes As DescriptionAttribute() = DirectCast(fi.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())
If attributes.Length > 0 Then
Return attributes(0).Description
End If
Return value.ToString()
End Function
The next item on our agenda (as a programmer you gotta love this word play! No? Oki, let's move along) is to show all the Enum's
in the ComboBox
. The main way that it seems to be done, at least by a massive amount of Google searches, is by the usage of ObjectDataProvider
:
<UserControl.Resources>
<ObjectDataProvider MethodName="GetValues"
ObjectType="{x:Type sys:Enum}" x:Key="Options">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:EnumOptions" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</UserControl.Resources>
And then bind to the ComboBox like so:
<ComboBox x:Name="cmbOptions"
ItemsSource="{Binding Source={StaticResource Options}}"
....
....
</ComboBox>
Oh my, that's a lot of writing to show the Enum
. Unless you decided to be a programmer because you absolutely loved typing commands to the computer, this is a bit much. I always wanted it to be along the following path; You declared an Enum
property inside your class:
Class TheComboBoxShowCase
Implements System.ComponentModel.INotifyPropertyChanged
...
Private pTheEnumProperty As TheComboBoxShow = TheComboBoxShow.On
Public Property TheEnumProperty() As TheComboBoxShow
Get
Return pTheEnumProperty
End Get
Set(ByVal value As TheComboBoxShow)
pTheEnumProperty = value
OnPropertyChanged("TheEnumProperty")
End Set
End Property
Public Enum TheComboBoxShow
<DescriptionAttribute("The show is on")>
[On]
<DescriptionAttribute("The Show is off")>
[Off]
End Enum
End Class
Then in XAML you just connected it to the ComboBox
with the following command:
<ComboBox Name="cmbEnum" ItemsSource="{Binding TheEnumProperty}" />
And then your property in your class would get updated if you changed the value in the ComboBox
, and if you changed the value in code behind it would update the selection. Well, it can happen with the help of an attached property:
Imports System.Reflection
Imports System.ComponentModel
Public Class EnumToComboBoxBinding
Private Shared Combo As ComboBox
Private Shared ComboNameList As List(Of String)
Private Shared ComboEnumList As List(Of [Enum])
Public Shared ReadOnly EnumItemsSourceProperty As DependencyProperty =
DependencyProperty.RegisterAttached("EnumItemsSource",
GetType([Enum]),
GetType(EnumToComboBoxBinding),
New FrameworkPropertyMetadata(Nothing,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
New PropertyChangedCallback(AddressOf OnEnumItemsSourceChanged)))
...
Public Shared Sub OnEnumItemsSourceChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim TempEnum As [Enum] = sender.GetValue(EnumItemsSourceProperty)
If ComboEnumList Is Nothing OrElse Not ComboEnumList.Contains(TempEnum) Then
If Combo IsNot Nothing Then
RemoveHandler Combo.SelectionChanged, AddressOf EnumValueChanged
End If
Combo = DirectCast(sender, ComboBox)
ComboNameList = New List(Of String)
ComboEnumList = New List(Of [Enum])
Dim Values = [Enum].GetValues(TempEnum.GetType)
For Each Value In Values
ComboNameList.Add(GetDescription(Value))
ComboEnumList.Add(Value)
Next
AddHandler Combo.SelectionChanged, AddressOf EnumValueChanged
Combo.ItemsSource = ComboNameList
End If
Combo.SelectedIndex = ComboEnumList.IndexOf(TempEnum)
End Sub
Private Shared Sub EnumValueChanged(sender As Object, e As EventArgs)
SetEnumItemsSource(Combo, ComboEnumList(Combo.SelectedIndex))
End Sub
...
End Class
The class is very straight forward, and I only eliminated the functions "we all know by now". All the Enum
values is taken from the property, and the result is stored in two lists, one for the ComboBox
display and one for the actual Enum
values available in the property. The display are hooked up, and an event is attached to the selection changes of the ComboBox
. And that's ALL FOLKS, it now works simply by typing in the code in XAML:
<ComboBox Name="cmbEnum"
local:EnumToComboBoxBinding.EnumItemsSource="{Binding TheEnumProperty}"
...
/>
and I employed a little trick to just test the single class by setting the DataContext
of the ComboBox
:
Dim TheShowCaseClass As New TheComboBoxShowCase
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
...
cmbEnum.DataContext = TheShowCaseClass
This little trick should make it easy to reuse it in all cases, or simply expand it with the additional functions you need.
The searchable TreeView
I was browsing through some articles about TreeView's
when I came across this article called:
I absolutely loved the functionality of the filter based search he provided, and my first thought was:I like to add this in my projects as well. But that wasn't so easy (well that isn't quite true, but you would have to do quite a bit of work in order to make it happen in your case as well) if you hadn't already thought about it from the start. I decided to ditch the fancy search box he had, I had no need for it. I just needed a search based on changes in a TextBox
, and the other stuff could easily be implemented as an afterthought, if the search field was up and running already.
So I started working on the helper class for the attached property that I wanted to create. It was clear to me that I needed to store each item in the TreeView
with the children element. I also needed the underlying class that was binded to each of the TreeViewItems
, and finally the search functionality that Fredrik had provided.
The class also needed to contain the reflection based search through properties of the type string. The helper class ended up looking like this, with pointers to the real values:
Public Class TreeViewHelperClass
Private pCurrentTreeViewItem As TreeViewItem
Public Property CurrentTreeViewItem() As TreeViewItem
Get
Return pCurrentTreeViewItem
End Get
Set(ByVal value As TreeViewItem)
pCurrentTreeViewItem = value
End Set
End Property
Private pBindedClass As Object
Public Property BindedClass() As Object
Get
Return pBindedClass
End Get
Set(ByVal value As Object)
pBindedClass = value
End Set
End Property
Private pChildren As New List(Of TreeViewHelperClass)
Public Property Children() As List(Of TreeViewHelperClass)
Get
Return pChildren
End Get
Set(ByVal value As List(Of TreeViewHelperClass))
pChildren = value
End Set
End Property
Private Function FindString(obj As Object, ByVal SearchString As String) As Boolean
If String.IsNullOrEmpty(SearchString) Then
Return True
End If
If obj Is Nothing Then Return True
For Each p As System.Reflection.PropertyInfo In obj.GetType().GetProperties()
If p.PropertyType = GetType(String) Then
Dim value As String = p.GetValue(obj)
If value.ToLower.Contains(SearchString.ToLower) Then
Return True
End If
End If
Next
Return False
End Function
Private expanded As Boolean
Private match As Boolean = True
Private Function IsCriteriaMatched(criteria As String) As Boolean
Return FindString(BindedClass, criteria)
End Function
Public Sub ApplyCriteria(criteria As String, ancestors As Stack(Of TreeViewHelperClass))
If IsCriteriaMatched(criteria) Then
IsMatch = True
For Each ancestor In ancestors
ancestor.IsMatch = True
Next
Else
IsMatch = False
End If
ancestors.Push(Me)
For Each child In Children
child.ApplyCriteria(criteria, ancestors)
Next
ancestors.Pop()
End Sub
Public Property IsMatch() As Boolean
Get
Return match
End Get
Set(value As Boolean)
If match = value Then Return
match = value
If CurrentTreeViewItem IsNot Nothing Then
If match Then
CurrentTreeViewItem.Visibility = Visibility.Visible
Else
CurrentTreeViewItem.Visibility = Visibility.Collapsed
End If
End If
OnPropertyChanged("IsMatch")
End Set
End Property
Public ReadOnly Property IsLeaf() As Boolean
Get
Return Not Children.Any()
End Get
End Property
End Class
As you see from the class the filter uses Visibility
changes in order to clear items that doesn't match. It is also a top down search, where all the parent TreeViewItems
are found by iterating through the stack of previous elements.
The criteria for a match is now only matched by looking at String
elements in the binded class, but it can easily be expanded to search for particular values of properties by simply alter the search function a little. Assuming that you are looking for an item that has the property name id
and the value 45
, then you could simply type in id==45
into the search string:
Dim PropertyName As String
Dim ValueOfProperty As String
PropertyName = SearchString.Split("==")(0)
ValueOfProperty = SearchString.Split("==")(1)
For Each p As System.Reflection.PropertyInfo In obj.GetType().GetProperties()
If p.CanRead Then
If p.Name.ToLower = PropertyName.ToLower Then
Dim t As Type = If(Nullable.GetUnderlyingType(p.PropertyType), p.PropertyType)
Dim safeValue As Object = If((ValueOfProperty Is Nothing), Nothing, Convert.ChangeType(ValueOfProperty, t))
Dim f = p.GetValue(obj)
If safeValue IsNot Nothing Then
If f = safeValue Then
Return True
Else
Return False
End If
Else
End If
End If
Next
End If
Next
That was the easy part of the searchable TreeView
, now we need to write the Attached dependency property and get the all the TreeViewItems
and underlying classes. It was pretty clear that we needed a SearchString
as the Attached dependency property, and that we need to have a new search every time the property changed.
The more difficult issue her is making sure that all the elements in the TreeView
is visible and that they are all drawn up when we try to populate the items into the helper class. Here Bea Stollniz's blog was a big help, so I implemented the function below, and now I could be fairly certain that all the elements was visible and expanded.
ApplyActionToAllTreeViewItems(Sub(itemsControl)
itemsControl.IsExpanded = True
itemsControl.Visibility = Visibility.Visible
DispatcherHelper.WaitForPriority(DispatcherPriority.ContextIdle)
End Sub, TreeViewControl)
An MSDN article about finding an item in the TreeView also explains (indirectly?) how to ensure that the element is populated.
To populate the items into the helper class, I found a MSDN FAQ that was much more linear in getting the elements.
Private Shared Sub CreateInternalViewModelFilter(parentContainer As ItemsControl, ByRef ParentTreeItem As TreeViewHelperClass)
For Each item As [Object] In parentContainer.Items
Dim TreeViewItemHelperContainer As New TreeViewHelperClass()
TreeViewItemHelperContainer.BindedClass = item
Dim currentContainer As TreeViewItem = TryCast(parentContainer.ItemContainerGenerator.ContainerFromItem(item), TreeViewItem)
TreeViewItemHelperContainer.CurrentTreeViewItem = currentContainer
ParentTreeItem.Children.Add(TreeViewItemHelperContainer)
If currentContainer IsNot Nothing AndAlso currentContainer.Items.Count > 0 Then
If currentContainer.ItemContainerGenerator.Status <> GeneratorStatus.ContainersGenerated Then
AddHandler currentContainer.ItemContainerGenerator.StatusChanged, Sub()
CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
End Sub
Else
CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
End If
End If
Next
End Sub
The only thing left then was to run the actual filter (or search):
TreeViewHelper.Item(0).ApplyCriteria(TempSearchString, New Stack(Of TreeViewHelperClass))
The Windows Form style TreeView for WPF
This TreeView started out with a style generated by Niel Kronlage from Microsoft that drew lines and added a ToggleButton for the expand and retract child TreeViewItems. He had also implemented a ValueConverter
to get the last item, as a means to stop drawing the lines.
This wored well for a static TreeView
that didn't add any new items. As there was no way of the TreeVeiw updating it's render, Alex P. (in the same thread) created and Attatched Property and added an eventhandler in the constructor, so that changes in the collection would force the UI to update.
Then we have the last addition (before me) which was made by TuyenTk and published here on CodeProject as a Tip: WPF TreeView with WinForms Style Fomat. He made some style changes but missed the Attatched property that updated the UI once you added new TreeViewITems.
Once I had implemented all the different parts from the others, I ued it when filtering/searching my TreeView
. It turen out that it didnt go so well, as the UI didn't know about the collapsed items. I needed to attach and event on Visibility changed.
I also moved things around a bit, to make the re-usability better, all you have to do now is to merge the directories that holds the style in Application:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/WinFormStyleTreeView/ExpandCollapseToggleStyle.xaml"/>
<ResourceDictionary Source="/WinFormStyleTreeView/WinFormStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
And be sure to add the ExpandCollapseToggleStyle
first as it is used by the WinFormStyle
. If you change it you will get a rather strange sounding error. The style can now be implemented on any of your TreeView's
separately like this:
<TreeView>
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource WinFormTreeView}"/>
...
</TreeView.Resources>
</TreeView>
In the attached class, were our attached properties lives, we need to have a value that will indicate if it has any items below itself. If so the property, IsLast
is set to false, otherwise it's set to true. If this isn't done, it sill simply draw the lines until it reaches the bottom TreeViewItem
in the control. So the IsLast
Dependency Property is set up:
Public Shared IsLastOneProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsLastOne", GetType(Boolean), GetType(TVIExtender))
Public Shared Function GetIsLastOne(sender As DependencyObject) As Boolean
Return CBool(sender.GetValue(IsLastOneProperty))
End Function
Public Shared Sub SetIsLastOne(sender As DependencyObject, isLastOne As Boolean)
sender.SetValue(IsLastOneProperty, isLastOne)
End Sub
However, we need this event to fire if the collection is changed, and the best way to hook the event up would be to place it in the constructor Sub New()
. The common way to do this is to attach a boolean dependency property called IsUsed or something similar. This should only be set once, when the object holding the attached dependency property is initiated, and you can have a CallBackFunction
to set up initial bindings on item created.
Alex P. does this trough reacting to changes in the UseExtenderProperty
, and initiates a new TVIExtender
with the TreeViewItem
as it's argument:
Private _item As TreeViewItem
Public Sub New(item As TreeViewItem)
_item = item
Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
AddHandler ic.ItemContainerGenerator.ItemsChanged, AddressOf OnItemsChangedItemContainerGenerator
_item.SetValue(IsLastOneProperty, ic.ItemContainerGenerator.IndexFromContainer(_item) = ic.Items.Count - 1)
End Sub
The code works simply by getting the TreeViewItem that holds the newly constructed child (also a TreeViewItem
) using the ItemsContol.ItemsControlFromItemContainer
. Then it adds a handler to the item changed, that fire each time the collection changes, and If the current item is the last in the collection, the IsLastproperty
is set to true. And if the collection is changed we are back to the same setting again:
Private Sub OnItemsChangedItemContainerGenerator(sender As Object, e As ItemsChangedEventArgs)
Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
If ic IsNot Nothing Then
_item.SetValue(IsLastOneProperty, ic.ItemContainerGenerator.IndexFromContainer(_item) = ic.Items.Count - 1)
End If
End Sub
So far I have just explained what Alex P. has done in his implementation of the Attached DependencyProperty, and as it is now it won't react correctly if one changes the Visiblility
of a TreeViewItem
. We must then recalculate the IsLast
property if the visibility value changes from Visible
or Hidden
(they will have the TreeView rendered the same way) to Collapsed
. To attatch an event to changes in a DependencyProperty I used the DependencyPropertyDescriptor class.
Private Shared VisibilityDescriptor As DependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(TreeViewItem.VisibilityProperty, GetType(TreeViewItem))
I added code to bind the VisibilityChange to a sub, in the constructor:
Public Sub New(item As TreeViewItem)
...
VisibilityDescriptor.AddValueChanged(_item, AddressOf VisibilityChanged)
...
End Sub
This would now run the sub VisibilityChanged
each time, a word of warning however. Each time you add an event or a Descriptor don't forget to mop up after you are done with it.
Private Sub Detach()
If _item IsNot Nothing Then
Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
If ic IsNot Nothing Then
RemoveHandler ic.ItemContainerGenerator.ItemsChanged, AddressOf OnItemsChangedItemContainerGenerator
End If
VisibilityDescriptor.RemoveValueChanged(_item, AddressOf VisibilityChanged)
End If
End Sub
Now that we have a sub that is run each time the visibility changes we start off with writing the code:
Private Sub VisibilityChanged(sender As Object, e As EventArgs)
If TypeOf (sender) Is TreeView Then
Exit Sub
End If
If DirectCast(_item, ItemsControl).Visibility = Visibility.Collapsed Then
Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
Dim Index As Integer = ic.ItemContainerGenerator.IndexFromContainer(_item)
If Index <> 0 And _item.GetValue(IsLastOneProperty) Then
DirectCast(ic.ItemContainerGenerator.ContainerFromIndex(Index - 1), TreeViewItem).SetValue(IsLastOneProperty, True)
End If
Else
Dim ic As ItemsControl = ItemsControl.ItemsControlFromItemContainer(_item)
Dim Index As Integer = ic.ItemContainerGenerator.IndexFromContainer(_item)
If Index <> 0 Then
DirectCast(ic.ItemContainerGenerator.ContainerFromIndex(Index - 1), TreeViewItem).SetValue(IsLastOneProperty, False)
End If
End If
End Sub
The code in itself is actually really simple, if the property IsLast is true, set the IsLast to true on the previous element, unless the current element is the only one in the collection. The other way around you can just set the previous element to false regardless of what it's value was. And that is all you need to have a functioning TreeView
when items are collapsible.
There is one more issue with the use of the DependencyPropertyDescriptor regards to detaching the event. I found Andrew Smith's blog entery from 2008, and translated that code to VB, and its in the Unused folder. However, 2008 is a long time ago, and articles on MSDN after it was published, still used the original method, so I left it as it is. If anyone know if it has been fixed, or if it's still a problem I'd really like to know about it.
Adding a pixel shader magnifier
Several years ago I remember seeing a Silverlight article that had the coolest looking magnifier glass I had ever seen, and it was create using pixel shaders. I didnt have the time to dig into the code so I just bookmarked the site and forgot about it all. Then as I did reasearch to this article, I came across this article: WPF Parent Window Shading Using Pixel Shaders, and I started thinking. Do I still have the link to that article with the cool magnifier? I did: Behaviors and Triggers in Silverlight, so let's get going and implement it now.
The source coe, you know the .fx file, that the DirectX compiler makes into a .ps file (you can read more about the compiling of these here. I will go through the bare minimum for you to start to understand what you need to take into account just to make them work, for a more detailed review and some cool tools and program see these links:
The (complete!) file looke like this:
float2 center : register(C0);
float inner_radius: register(C2);
float magnification : register(c3);
float outer_radius : register(c4);
SamplerState Input : register(S0);
float4 main( float2 uv : TEXCOORD) : COLOR
{
float2 center_to_pixel = uv - center; float distance = length(center_to_pixel);
float4 color;
float2 sample_point;
if(distance < outer_radius)
{
if( distance < inner_radius )
{
sample_point = center + (center_to_pixel / magnification);
}
else
{
float radius_diff = outer_radius - inner_radius;
float ratio = (distance - inner_radius ) / radius_diff; ratio = ratio * 3.14159; float adjusted_ratio = cos( ratio ); adjusted_ratio = adjusted_ratio + 1; adjusted_ratio = adjusted_ratio / 2;
sample_point = ( (center + (center_to_pixel / magnification) ) * ( adjusted_ratio)) + ( uv * ( 1 - adjusted_ratio) );
}
}
else
{
sample_point = uv;
}
return tex2D( Input, sample_point );
}
At the very start you see 4 input parameters into the code, named C0,C2,C3 and C4, and these will link up to their separate Dependency Properties. The variable marked Input is an "image register" that is also linked to a Dependency Property. The dependency properties look like this:
Public Shared ReadOnly CenterProperty As DependencyProperty =
DependencyProperty.Register("Center",
GetType(Point),
GetType(Magnifier),
New PropertyMetadata(New Point(0.5, 0.5),
PixelShaderConstantCallback(0)))
Public Shared ReadOnly InnerRadiusProperty As DependencyProperty =
DependencyProperty.Register("InnerRadius",
GetType(Double),
GetType(Magnifier),
New PropertyMetadata(0.2, PixelShaderConstantCallback(2)))
You see that the
PixelShaderConstantCallback
has the same value in the functon that the C variables had in the fx file. In the same file (Magnifier) we also reset the PixelShader property (which is inhereted from the ShaderEffect class in the ShaderEffectBase):
Public MustInherit Class ShaderEffectBase
Inherits ShaderEffect
The shadereffects.PixelShader is just an empty pointer so it needs a new instance of PixelShader:
Sub New()
PixelShader = New PixelShader
PixelShader.UriSource = New Uri(AppDomain.CurrentDomain.BaseDirectory & "\ShaderSourceFiles\Magnifier.ps")
Me.UpdateShaderValue(CenterProperty)
Me.UpdateShaderValue(InnerRadiusProperty)
Me.UpdateShaderValue(OuterRadiusProperty)
Me.UpdateShaderValue(MagnificationProperty)
End Sub
The UriSource is a real pain, it needs to find the compiled file *.ps at that spesific location, otherwise your program will crash.
Now its just the class with the Attached Dependency Property left, and I will initiate it (as per usual) with a bool value set to true:
Public Shared MagnifyProperty As DependencyProperty =
DependencyProperty.RegisterAttached("Magnify",
GetType(Boolean),
GetType(MagnifierOverBehavior),
New FrameworkPropertyMetadata(AddressOf MagnifiedChanged))
And the callback to MagnifiedChanged we attach and detach the handlers used:
Public Shared Sub MagnifiedChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
AssociatedObject = TryCast(sender, FrameworkElement)
If AssociatedObject IsNot Nothing Then
If e.NewValue Then
OnAttached()
Else
OnDetaching()
End If
End If
End Sub
The two classes are exact opposite of one another:
Private Shared Sub OnAttached()
AddHandler AssociatedObject.MouseEnter, AddressOf AssociatedObject_MouseEnter
AddHandler AssociatedObject.MouseLeave, AddressOf AssociatedObject_MouseLeave
AddHandler AssociatedObject.MouseMove, AddressOf AssociatedObject_MouseMove
AssociatedObject.Effect = magnifier
End Sub
Private Shared Sub OnDetaching()
RemoveHandler AssociatedObject.MouseEnter, AddressOf AssociatedObject_MouseEnter
RemoveHandler AssociatedObject.MouseLeave, AddressOf AssociatedObject_MouseLeave
RemoveHandler AssociatedObject.MouseMove, AddressOf AssociatedObject_MouseMove
AssociatedObject.Effect = Nothing
End Sub
This adds up to reactions in the mouse move section, that initiates a StoryBoard to move the Magnigication:
Private Shared Sub AssociatedObject_MouseMove(sender As Object, e As MouseEventArgs)
TryCast(AssociatedObject.Effect, Magnifier).Center = e.GetPosition(AssociatedObject)
Dim mousePosition As Point = e.GetPosition(AssociatedObject)
mousePosition.X /= AssociatedObject.ActualWidth
mousePosition.Y /= AssociatedObject.ActualHeight
magnifier.Center = mousePosition
Dim zoomInStoryboard As New Storyboard()
Dim zoomInAnimation As New DoubleAnimation()
zoomInAnimation.[To] = magnifier.Magnification
zoomInAnimation.Duration = TimeSpan.FromSeconds(0.5)
Storyboard.SetTarget(zoomInAnimation, AssociatedObject.Effect)
Storyboard.SetTargetProperty(zoomInAnimation, New PropertyPath(magnifier.MagnificationProperty))
zoomInAnimation.FillBehavior = FillBehavior.HoldEnd
zoomInStoryboard.Children.Add(zoomInAnimation)
zoomInStoryboard.Begin()
End Sub
The magnifier will magnify anyting that is a FrameworkElement, and can be turen on and off by a boolean values.