Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

Examples of Attached Behaviors in WPF

Rate me:
Please Sign up or sign in to vote.
4.87/5 (8 votes)
18 May 2015CPOL13 min read 29.5K   910   12   7
The following is achieved using attached properties: Binds Enum to ComboBox, make a searchable TreeView, create WinFormStyle TreeView for WPF and a cool pixel shader Magnification for WPF.

Image 1

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:

VB.NET
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:

VB.NET
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:

VB.NET
DependencyProperty.RegisterAttached

and below is valid for a normal dependency property

VB.NET
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:

XML
<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:

VB.NET
Public Shared Function GetDescription(value As [Enum]) As String
    ' Get information on the enum element
    Dim fi As FieldInfo = value.[GetType]().GetField(value.ToString())
    ' Get description for elum element
    Dim attributes As DescriptionAttribute() = DirectCast(fi.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())
    If attributes.Length > 0 Then
        ' DescriptionAttribute exists - return that
        Return attributes(0).Description
    End If
    ' No Description set - return enum element name
    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:

XML
<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:

XML
<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:

VB.NET
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:

XML
<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:

VB.NET
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)
        'Store the set enum locally
        Dim TempEnum As [Enum] = sender.GetValue(EnumItemsSourceProperty)

        ' First time run trough or the binding source has changed (last one not very likely or impossible?)
        If ComboEnumList Is Nothing OrElse Not ComboEnumList.Contains(TempEnum) Then

            ' Remove any previously handlers
            If Combo IsNot Nothing Then
                RemoveHandler Combo.SelectionChanged, AddressOf EnumValueChanged
            End If

            Combo = DirectCast(sender, ComboBox)

            'Clear the lists
            ComboNameList = New List(Of String)
            ComboEnumList = New List(Of [Enum])

            'Get all possible values for the enum type
            Dim Values = [Enum].GetValues(TempEnum.GetType)

            ' Loop trough them and store the description 
            ' and the Enum type in two separate lists
            For Each Value In Values
                ComboNameList.Add(GetDescription(Value))
                ComboEnumList.Add(Value)
            Next

            'Add a handler if you change the selected value of the ComboBox
            AddHandler Combo.SelectionChanged, AddressOf EnumValueChanged

            ' Set the ComboBox's ItemsSource to the DescriptionAttribute
            Combo.ItemsSource = ComboNameList
        End If

        ' Sync the selected value with the Property 
        Combo.SelectedIndex = ComboEnumList.IndexOf(TempEnum)
    End Sub

    Private Shared Sub EnumValueChanged(sender As Object, e As EventArgs)
        ' Selected item in the ComboBox has changes, so updates the  Enum DependencyProperty
        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:

XML
        <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:
VB.NET
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:

VB.NET
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) ' and then just touch 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:

VB.NET
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))

                'Get the value
                Dim f = p.GetValue(obj)

                'Its the same type
                If safeValue IsNot Nothing Then
                    If f = safeValue Then
                        Return True
                    Else
                        Return False
                    End If
                Else
                    ' If you end up here you have entered the wrong element type of the property
                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.

VB.NET
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.

VB.NET
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

                ' This indicates that the TreeView isn't fully created yet.
                ' That means that the code should not have reached this point

                ' If the sub containers of current item is not ready, we need to wait until
                ' they are generated.
                AddHandler currentContainer.ItemContainerGenerator.StatusChanged, Sub()
                                                                                      CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
                                                                                  End Sub
            Else
                ' If the sub containers of current item is ready, we can directly go to the next
                ' iteration to expand them.
                CreateInternalViewModelFilter(currentContainer, TreeViewItemHelperContainer)
            End If

        End If
    Next
End Sub

The only thing left then was to run the actual filter (or search):

VB.NET
'The first instance is a dummy that is not connected to the TreeView, but can initiate the 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:

XML
<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:

XML
<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:

VB.NET
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:

VB.NET
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:

VB.NET
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.

VB.NET
Private Shared VisibilityDescriptor As DependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(TreeViewItem.VisibilityProperty, GetType(TreeViewItem))

I added code to bind the VisibilityChange to a sub, in the constructor:

VB.NET
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.

VB.NET
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:

VB.NET
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:

C++
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; // vector from center to pixel  
    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; // 0 == inner radius, 1 == outer_radius
          ratio = ratio * 3.14159; //  -pi/2 .. pi/2          
          float adjusted_ratio = cos( ratio );  // -1 .. 1
          adjusted_ratio = adjusted_ratio + 1;   // 0 .. 2
          adjusted_ratio = adjusted_ratio / 2;   // 0 .. 1
       
          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:

VB.NET
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 

VB.NET
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):

VB.NET
Public MustInherit Class ShaderEffectBase
    Inherits ShaderEffect

The shadereffects.PixelShader is just an empty pointer so it needs a new instance of PixelShader: 

VB.NET
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:

VB.NET
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:

VB.NET
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:

VB.NET
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:

VB.NET
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Norway Norway
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralI'll redirect reader from my article to your! Pin
TuyenTk21-May-15 6:04
professionalTuyenTk21-May-15 6:04 
GeneralRe: I'll redirect reader from my article to your! Pin
Kenneth Haugland21-May-15 6:35
mvaKenneth Haugland21-May-15 6:35 
AnswerRe: I'll redirect reader from my article to your! Pin
TuyenTk21-May-15 17:54
professionalTuyenTk21-May-15 17:54 
GeneralRe: I'll redirect reader from my article to your! Pin
Kenneth Haugland25-May-15 8:38
mvaKenneth Haugland25-May-15 8:38 
GeneralRe: I'll redirect reader from my article to your! Pin
TuyenTk25-May-15 21:08
professionalTuyenTk25-May-15 21:08 
QuestionI get it now Pin
Fredrik Bornander19-May-15 20:38
professionalFredrik Bornander19-May-15 20:38 
I get what you mean now when you said "if it is possible to hide all the code in the TreeView itself" (in the discussion we had on my article), you didn't need the text box for entering the criteria, you were just looking for a general way to do the node expanding based on some criteria. Neat!

I like your approach, and I'll use it with the modification of being able to inject a comparator-class that can do custom comparison on the node rather than iterating over all properties.
Try Hovercraft for Android, voted "a game" by players.

AnswerRe: I get it now Pin
Kenneth Haugland20-May-15 0:17
mvaKenneth Haugland20-May-15 0:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.