Edit this page

Integration with RadContextMenu

The purpose of this tutorial is to show you how to integrate RadChart and RadContextMenu so they can nicely work together.

The final result should look like the snapshot below.

Basically, the approach that will be demonstrated here combines the MVVM support of RadChart with RadChart data-binding, and the data-binding functionality supported by the RadMenu (RadContextMenu) control.

You will need several helper classes that will be used for the RadChart and RadContextMenu population.

  • Create a new class named MenuItem. This class represents a single item in the RadContextMenu. Note that the class has absolutely the same properties as the RadMenuItem used in the context menu. The idea here is to bind the different properties of the MenuItem business object to the corresponding properties of the RadMenuItem using style binding.

public class MenuItem : INotifyPropertyChanged
{
    private bool isChecked;
    private bool isEnabled = true;
    private string text;
    private string groupName;
    private bool isCheckable;
    private bool isSeparator;
    private string imageUrl;
    private bool staysOpenOnClick;
    private MenuItemsCollection items;
    private MenuItem parent;
    public MenuItem()
    {
        this.items = new MenuItemsCollection( this );
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public string Text
    {
        get
        {
            return this.text;
        }
        set
        {
            this.text = value;
        }
    }
    public string GroupName
    {
        get
        {
            return this.groupName;
        }
        set
        {
            this.groupName = value;
        }
    }
    public bool IsCheckable
    {
        get
        {
            return this.isCheckable;
        }
        set
        {
            this.isCheckable = value;
        }
    }
    public bool IsSeparator
    {
        get
        {
            return this.isSeparator;
        }
        set
        {
            this.isSeparator = value;
        }
    }
    public string ImageUrl
    {
        get
        {
            return this.imageUrl;
        }
        set
        {
            this.imageUrl = value;
        }
    }
    public bool StaysOpenOnClick
    {
        get
        {
            return this.staysOpenOnClick;
        }
        set
        {
            this.staysOpenOnClick = value;
        }
    }
    public MenuItemsCollection Items
    {
        get
        {
            return this.items;
        }
    }
    public MenuItem Parent
    {
        get
        {
            return this.parent;
        }
        set
        {
            this.parent = value;
        }
    }
    public bool IsEnabled
    {
        get
        {
            return this.isEnabled;
        }
        set
        {
            if ( value != this.isEnabled )
            {
                this.isEnabled = value;
                this.OnPropertyChanged( "IsEnabled" );
            }
        }
    }
    public bool IsChecked
    {
        get
        {
            return this.isChecked;
        }
        set
        {
            if ( value != this.isChecked )
            {
                this.isChecked = value;
                this.OnPropertyChanged( "IsChecked" );
                if ( !string.IsNullOrEmpty( this.GroupName ) )
                {
                    if ( this.IsChecked )
                    {
                        this.UncheckOtherItemsInGroup();
                    }
                    else
                    {
                        this.IsChecked = true;
                    }
                }
            }
        }
    }
    public Image Image
    {
        get
        {
            if ( string.IsNullOrEmpty( this.ImageUrl ) )
                return null;
            return new Image()
            {
                Source = new BitmapImage( new Uri( this.ImageUrl, UriKind.RelativeOrAbsolute ) )
            };
        }
    }
    private void UncheckOtherItemsInGroup()
    {
        IEnumerable<MenuItem> groupItems = this.Parent.Items.Where( ( MenuItem item ) => item.GroupName == this.GroupName );
        foreach ( MenuItem item in groupItems )
        {
            if ( item != this )
            {
                item.isChecked = false;
                item.OnPropertyChanged( "IsChecked" );
            }
        }
    }
    private void OnPropertyChanged( string propertyName )
    {
        if ( null != this.PropertyChanged )
        {
            this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }
}
Public Class MenuItem
    Implements INotifyPropertyChanged

    Private m_isChecked As Boolean
    Private m_isEnabled As Boolean = True
    Private m_text As String
    Private m_groupName As String
    Private m_isCheckable As Boolean
    Private m_isSeparator As Boolean
    Private m_imageUrl As String
    Private m_staysOpenOnClick As Boolean
    Private m_items As MenuItemsCollection
    Private m_parent As MenuItem

    Public Sub New()
        Me.m_items = New MenuItemsCollection(Me)
    End Sub

    Public Property Text() As String
        Get
            Return Me.m_text
        End Get
        Set(ByVal value As String)
            Me.m_text = value
        End Set
    End Property

    Public Property GroupName() As String
        Get
            Return Me.m_groupName
        End Get
        Set(ByVal value As String)
            Me.m_groupName = value
        End Set
    End Property

    Public Property IsCheckable() As Boolean
        Get
            Return Me.m_isCheckable
        End Get
        Set(ByVal value As Boolean)
            Me.m_isCheckable = value
        End Set
    End Property

    Public Property IsSeparator() As Boolean
        Get
            Return Me.m_isSeparator
        End Get
        Set(ByVal value As Boolean)
            Me.m_isSeparator = value
        End Set
    End Property

    Public Property ImageUrl() As String
        Get
            Return Me.m_imageUrl
        End Get
        Set(ByVal value As String)
            Me.m_imageUrl = value
        End Set
    End Property

    Public Property StaysOpenOnClick() As Boolean
        Get
            Return Me.m_staysOpenOnClick
        End Get
        Set(ByVal value As Boolean)
            Me.m_staysOpenOnClick = value
        End Set
    End Property

    Public ReadOnly Property Items() As MenuItemsCollection
        Get
            Return Me.m_items
        End Get
    End Property

    Public Property Parent() As MenuItem
        Get
            Return Me.m_parent
        End Get
        Set(ByVal value As MenuItem)
            Me.m_parent = value
        End Set
    End Property

    Public Property IsEnabled() As Boolean
        Get
            Return Me.m_isEnabled
        End Get
        Set(ByVal value As Boolean)
            If value <> Me.m_isEnabled Then
                Me.m_isEnabled = value
                Me.OnPropertyChanged("IsEnabled")
            End If
        End Set
    End Property

    Public Property IsChecked() As Boolean
        Get
            Return Me.m_isChecked
        End Get
        Set(ByVal value As Boolean)
            If value <> Me.m_isChecked Then
                Me.m_isChecked = value
                Me.OnPropertyChanged("IsChecked")

                If Not String.IsNullOrEmpty(Me.GroupName) Then
                    If Me.IsChecked Then
                        Me.UncheckOtherItemsInGroup()
                    Else
                        Me.IsChecked = True
                    End If
                End If
            End If
        End Set
    End Property

    Public ReadOnly Property Image() As Image
        Get
            If String.IsNullOrEmpty(Me.ImageUrl) Then
                Return Nothing
            End If

            Return New Image()
        End Get
    End Property

    Private Sub UncheckOtherItemsInGroup()
        Dim groupItems As IEnumerable(Of MenuItem) = Me.Parent.Items.Where(Function(item As MenuItem) item.GroupName = Me.GroupName)
        For Each item As MenuItem In groupItems
            If item IsNot Me Then
                item.IsChecked = False
                item.OnPropertyChanged("IsChecked")
            End If
        Next
    End Sub

    Private Sub OnPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
End Class
  • Create a new class named MenuItemCollection. This class is pretty simple and self-explanatory.

public class MenuItemsCollection : ObservableCollection<MenuItem>
{
    private MenuItem parent;
    public MenuItemsCollection()
        : this( null )
    {
    }
    public MenuItemsCollection( MenuItem parent )
    {
        this.parent = parent;
    }
    public MenuItem Parent
    {
        get
        {
            return this.parent;
        }
        set
        {
            this.parent = value;
        }
    }
    protected override void InsertItem( int index, MenuItem item )
    {
        item.Parent = this.Parent;
        base.InsertItem( index, item );
    }
}
Public Class MenuItemsCollection
    Inherits ObservableCollection(Of MenuItem)
    Private m_parent As MenuItem

    Public Sub New()
        Me.New(Nothing)
    End Sub

    Public Sub New(ByVal parent As MenuItem)
        Me.m_parent = parent
    End Sub

    Public Property Parent() As MenuItem
        Get
            Return Me.m_parent
        End Get
        Set(ByVal value As MenuItem)
            Me.m_parent = value
        End Set
    End Property

    Protected Overloads Overrides Sub InsertItem(ByVal index As Integer, ByVal item As MenuItem)
        item.Parent = Me.Parent
        MyBase.InsertItem(index, item)
    End Sub
End Class
  • Create a new class named ChartDataItem. The ChartDataItem is used to populate the RadChart with sample data.

public class ChartDataItem : INotifyPropertyChanged
{
    private double yValue;
    private MenuItemsCollection menuItems;
    public event PropertyChangedEventHandler PropertyChanged;
    public double YValue
    {
        get
        {
            return this.yValue;
        }
        set
        {
            if ( this.yValue != value )
            {
                this.yValue = value;
                this.OnPropertyChanged( "YValue" );
            }
        }
    }
    public MenuItemsCollection MenuItems
    {
        get
        {
            if ( this.menuItems == null )
            {
                this.menuItems = new MenuItemsCollection();
            }
            return this.menuItems;
        }
    }
    private void OnPropertyChanged( string propertyName )
    {
        if ( null != this.PropertyChanged )
        {
            this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }
}
Public Class ChartDataItem
    Implements INotifyPropertyChanged
    Private m_yValue As Double
    Private m_menuItems As MenuItemsCollection

    Public Property YValue() As Double
        Get
            Return Me.m_yValue
        End Get
        Set(ByVal value As Double)
            If Me.m_yValue <> value Then
                Me.m_yValue = value
                Me.OnPropertyChanged("YValue")
            End If
        End Set
    End Property

    Public ReadOnly Property MenuItems() As MenuItemsCollection
        Get
            If Me.m_menuItems Is Nothing Then
                Me.m_menuItems = New MenuItemsCollection()
            End If

            Return Me.m_menuItems
        End Get
    End Property

    Private Sub OnPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
End Class
  • Create a new class named ChartDataCollection - this is an observable collection of ChartDataItem objects.

public class ChartDataCollection : ObservableCollection<ChartDataItem>
{
}
Public Class ChartDataCollection
    Inherits ObservableCollection(Of ChartDataItem)
End Class
  • And the final prerequisite is the ChartViewModel - the data source for your RadChart.

public class ChartViewModel
{
    private Random rand = new Random( DateTime.Now.Millisecond );
    private ChartDataCollection data;
    public ChartDataCollection Data
    {
        get
        {
            if ( data == null )
            {
                data = new ChartDataCollection();
                data.Add( CreateChartDataItem( "Test {0}" ) );
                data.Add( CreateChartDataItem( "Foo {0}" ) );
                data.Add( CreateChartDataItem( "Bar {0}" ) );
            }
            return data;
        }
    }
    private ChartDataItem CreateChartDataItem( string menuItemTextFormat )
    {
        ChartDataItem item = new ChartDataItem();
        item.YValue = rand.Next( 10, 100 );
        for ( int i = 0; i < rand.Next( 2, 5 ); i++ )
        {
            item.MenuItems.Add( new MenuItem()
            {
                Text = string.Format( menuItemTextFormat, i )
            } );
        }
        return item;
    }
}
Public Class ChartViewModel
    Private rand As New Random(DateTime.Now.Millisecond)
    Private m_data As ChartDataCollection

    Public ReadOnly Property Data() As ChartDataCollection
        Get
            If m_data Is Nothing Then
                m_data = New ChartDataCollection()

                m_data.Add(CreateChartDataItem("Test {0}"))
                m_data.Add(CreateChartDataItem("Foo {0}"))
                m_data.Add(CreateChartDataItem("Bar {0}"))
            End If

            Return m_data
        End Get
    End Property

    Private Function CreateChartDataItem(ByVal menuItemTextFormat As String) As ChartDataItem
        Dim item As New ChartDataItem()

        item.YValue = rand.Next
        For i As Integer = 0 To rand.Next - 1
            item.MenuItems.Add(New MenuItem())
        Next

        Return item
    End Function
End Class
  • Now you need to re-template the desired chart series type in order to place a RadContextMenu instance as attached property in its template.

<Style x:Key="CustomStyle"
       TargetType="telerik:Bar">
    <Setter Property="Template">
        <Setter.Value>
                    <ControlTemplate TargetType="telerik:Bar">
                <Canvas Opacity="0"
                        x:Name="PART_MainContainer">
                            <telerik:RadContextMenu.ContextMenu>
                                <telerik:RadContextMenu ItemsSource="{Binding DataItem.MenuItems}"
                                                          ItemTemplate="{StaticResource MenuItemTemplate}"
                                                          ItemClick="OnContextMenuClick">
                                    <telerik:RadContextMenu.ItemContainerStyle>
                                            <Style TargetType="telerik:RadMenuItem">
                                    <Setter Property="IsCheckable"
                                            Value="{Binding IsCheckable}" />
                                    <Setter Property="IsChecked"
                                            Value="{Binding IsChecked}" />
                                    <Setter Property="IsSeparator"
                                            Value="{Binding IsSeparator}" />
                                    <Setter Property="IsEnabled"
                                            Value="{Binding IsEnabled}" />
                                    <Setter Property="StaysOpenOnClick"
                                            Value="{Binding StaysOpenOnClick}" />
                                    <Setter Property="Icon"
                                            Value="{Binding Image}" />
                                </Style>
                            </telerik:RadContextMenu.ItemContainerStyle>
                        </telerik:RadContextMenu>
                    </telerik:RadContextMenu.ContextMenu>
                    <Rectangle x:Name="PART_DefiningGeometry"
                               Height="{TemplateBinding ItemActualHeight}"
                               Width="{TemplateBinding ItemActualWidth}"
                               Style="{TemplateBinding ItemStyle}"
                               RadiusX="{StaticResource BarRadiusX}"
                               RadiusY="{StaticResource BarRadiusY}" />
                    <Rectangle Height="{TemplateBinding ItemActualHeight}"
                               Width="{TemplateBinding ItemActualWidth}"
                               RadiusX="{StaticResource BarRadiusX}"
                               RadiusY="{StaticResource BarRadiusY}"
                               OpacityMask="{StaticResource BarOpacityMaskBrush}"
                               Fill="{StaticResource BarMaskBrush}" />
                    <Rectangle Height="{TemplateBinding ItemActualHeight}"
                               Width="{TemplateBinding ItemActualWidth}"
                               RadiusX="{StaticResource BarRadiusX}"
                               RadiusY="{StaticResource BarRadiusY}"
                               Fill="{StaticResource BarTopMaskBrush}" />
                    <Canvas.RenderTransform>
                        <ScaleTransform x:Name="PART_AnimationTransform"
                                        ScaleY="0" />
                    </Canvas.RenderTransform>
                </Canvas>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

You should note several things in the above code snippet:

  • Notice that the ContextMenu is data bound to a property of your chart DatabItem instance. The ChartDataItem object exposes the desired menu items' structure (i.e. the chart control is data bound to a List of ChartDataItem objects, and the ChartDataItem class exposes MenuItems property. {Binding____DataItem.MenuItems} holds a reference to a specific ChartDataItem instance).

  • The RadContextMenu can support hierarchical Menu structure as well (as with stand-alone Menu you need to create a HierarchicalDataTemplate that specifies the ItemsSource, ItemTemplate and other properties of the item's children).

  • For WPF you can use the style binding__mechanism that allows you to bind properties of an item container ( __RadMenuItem, RadTreeViewItem, etc.) to the properties of the data object. Also you should set the ItemContainerStyle property of the RadContextMenu.

The code snippet below shows you the full XAML code:

<Grid>
    <Grid.Resources>
        <HierarchicalDataTemplate x:Key="MenuItemTemplate"
                                  ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Text}" />
        </HierarchicalDataTemplate>
        <mscorlib:Double x:Key="BarRadiusX">2</mscorlib:Double>
        <mscorlib:Double x:Key="BarRadiusY">2</mscorlib:Double>
        <LinearGradientBrush x:Key="BarMaskBrush"
                             EndPoint="1,0.5"
                             StartPoint="0,0.5">
            <GradientStop Color="#00FFFFFF"
                          Offset="0" />
            <GradientStop Color="#00FFFFFF"
                          Offset="1" />
            <GradientStop Color="#26000000"
                          Offset="0.2" />
            <GradientStop Color="#66000000"
                          Offset="0.2" />
        </LinearGradientBrush>
        <SolidColorBrush x:Key="BarOpacityMaskBrush"
                         Color="#FF000000" />
        <SolidColorBrush x:Key="BarTopMaskBrush"
                         Color="Transparent" />
        <Style x:Key="CustomStyle"
               TargetType="telerik:Bar">
            <Setter Property="Template">
                <Setter.Value>
                        <ControlTemplate TargetType="telerik:Bar">
                        <Canvas Opacity="0"
                                x:Name="PART_MainContainer">
                                <telerik:RadContextMenu.ContextMenu>
                                    <telerik:RadContextMenu ItemsSource="{Binding DataItem.MenuItems}"
                                                                  ItemTemplate="{StaticResource MenuItemTemplate}"
                                                                  ItemClick="OnContextMenuClick">
                                        <telerik:RadContextMenu.ItemContainerStyle>
                                            <Style TargetType="telerik:RadMenuItem">
                                            <Setter Property="IsCheckable"
                                                    Value="{Binding IsCheckable}" />
                                            <Setter Property="IsChecked"
                                                    Value="{Binding IsChecked}" />
                                            <Setter Property="IsSeparator"
                                                    Value="{Binding IsSeparator}" />
                                            <Setter Property="IsEnabled"
                                                    Value="{Binding IsEnabled}" />
                                            <Setter Property="StaysOpenOnClick"
                                                    Value="{Binding StaysOpenOnClick}" />
                                            <Setter Property="Icon"
                                                    Value="{Binding Image}" />
                                        </Style>
                                    </telerik:RadContextMenu.ItemContainerStyle>
                                </telerik:RadContextMenu>
                            </telerik:RadContextMenu.ContextMenu>
                            <Rectangle x:Name="PART_DefiningGeometry"
                                       Height="{TemplateBinding ItemActualHeight}"
                                       Width="{TemplateBinding ItemActualWidth}"
                                       Style="{TemplateBinding ItemStyle}"
                                       RadiusX="{StaticResource BarRadiusX}"
                                       RadiusY="{StaticResource BarRadiusY}" />
                            <Rectangle Height="{TemplateBinding ItemActualHeight}"
                                       Width="{TemplateBinding ItemActualWidth}"
                                       RadiusX="{StaticResource BarRadiusX}"
                                       RadiusY="{StaticResource BarRadiusY}"
                                       OpacityMask="{StaticResource BarOpacityMaskBrush}"
                                       Fill="{StaticResource BarMaskBrush}" />
                            <Rectangle Height="{TemplateBinding ItemActualHeight}"
                                       Width="{TemplateBinding ItemActualWidth}"
                                       RadiusX="{StaticResource BarRadiusX}"
                                       RadiusY="{StaticResource BarRadiusY}"
                                       Fill="{StaticResource BarTopMaskBrush}" />
                            <Canvas.RenderTransform>
                                <ScaleTransform x:Name="PART_AnimationTransform"
                                                ScaleY="0" />
                            </Canvas.RenderTransform>
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>
        <telerik:RadChart x:Name="radChart"
                           ItemsSource="{Binding Data}">
            <telerik:RadChart.SeriesMappings>
                <telerik:SeriesMapping>
                    <telerik:SeriesMapping.SeriesDefinition>
                        <telerik:BarSeriesDefinition ItemStyle="{StaticResource CustomStyle}" />
                </telerik:SeriesMapping.SeriesDefinition>
                    <telerik:SeriesMapping.ItemMappings>
                        <telerik:ItemMapping FieldName="YValue"
                                                 DataPointMember="YValue" />
                </telerik:SeriesMapping.ItemMappings>
            </telerik:SeriesMapping>
        </telerik:RadChart.SeriesMappings>
    </telerik:RadChart>
</Grid>
  • What is left is to add the chart control declaration, bind it to a property of the ViewModel set as a DataContext for the respective user control, and instruct the control to use the custom bar style declared earlier:

<telerik:RadChart x:Name="radChart"
   ItemsSource="{Binding Data}">
    <telerik:RadChart.SeriesMappings>
        <telerik:SeriesMapping>
            <telerik:SeriesMapping.SeriesDefinition>
                <telerik:BarSeriesDefinition ItemStyle="{StaticResource CustomStyle}" />
            </telerik:SeriesMapping.SeriesDefinition>
            <telerik:SeriesMapping.ItemMappings>
                <telerik:ItemMapping FieldName="YValue"
                         DataPointMember="YValue" />
            </telerik:SeriesMapping.ItemMappings>
        </telerik:SeriesMapping>
    </telerik:RadChart.SeriesMappings>
</telerik:RadChart>

public void SetDataContex()
{
    this.DataContext = new ChartViewModel();
}
private void OnContextMenuClick(object sender, Telerik.Windows.RadRoutedEventArgs e)
{
    // Get the clicked item
    MenuItem menuItem = (e.OriginalSource as RadMenuItem).Header as MenuItem;
    if (menuItem.Text == "Foo 0")
    {
        //...
    }
}
Dim menu As RadContextMenu = CType(sender, RadContextMenu)
Dim clickedBar As Bar = menu.GetClickedElement(Of Bar)()
Dim clickedBarYValue As Double = clickedBar.DataPoint.YValue

You could download the complete source code for this tutorial here.