Rating control in WPF

This article gives you a step by step walkthrough how to create a Rating control like below in WPF. If you are new to custom control development, here is an excellent article to get the basic idea on Custom control development.

Rating2

Before we start, It is always good to separate custom controls from the project and maintain as a class library. I prefer below project structure for a custom control library.

Rating1

If a resource is not defined for a specific theme, then the control checks Classic.xaml for the resource. If the resource is not defined in the file that corresponds to the current theme or in Classic.xaml, the control uses the generic resource, which is in a resource dictionary file named generic.xaml.

Also it is very important that from where your components are derived. Here the Rating control is derived from ItemsControl, each ItemContainer will be replaced by a star symbol which is a ContentControl. This is achieved by overriding the GetContainerForItemOverride method.

 protected override DependencyObject GetContainerForItemOverride()
 {
    return new RatingItem();
 }

Rating control exposes RatingItemLimit, RatingItemBackground, RatingItemHighlightColor, RatingItemMouseDownColor and RatingValue properties to interact with the control.

<controls:Rating Width="250"
                 Height="20"
                 RatingItemLimit="10"
                 RaingItemBackground="Red"
                 RatingItemHighlightColor="Green"
                 RatingItemMouseDownColor="Black"
                 RatingValue="2.6">
</controls:Rating>

 

Get the complete source code here.

.Net 4.5 ‘Delay’ Binding property

‘Delay’ is one of the coolest property which is introduced as a part of the series on WPF 4.5 new features.

As pointed out by MSDN, If you use a data binding to update a data source, you can use the Delay property to specify an amount of time to pass after the property changes on the target before the source updates. For example, suppose that you have a Slider that has its Value property data two-way bound to a property of a data object and the UpdateSourceTrigger property is set to PropertyChanged. In this example, when the user moves the Slider, the source updates for each pixel that the Slider moves. The source object typically needs the value of the slider only when the slider’s Value stops changing. To prevent updating the source too often, use Delay to specify that the source should not be updated until a certain amount of time elapses after the thumb stops moving.

<Window x:Class="WpfApplication6.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="166"
        Width="301">
    <Grid>
        <StackPanel>
            <Slider x:Name="ValueSlider"
                    Minimum="0"
                    Maximum="100"
                    Margin="20"
                    Height="25"
                    Value="{Binding ElementName=ValueText, Delay=500, Path=Text,Mode=TwoWay}" />
            <TextBox x:Name="ValueText"
                     Text="50"
                     Width="100"
                     Height="50"
                     FontSize="20"
                     HorizontalAlignment="Center" />
        </StackPanel>
    </Grid>
</Window>

Again, it’s a discrete addition to the WPF framework but it’s a useful one !

Markup Extensions for events in .Net 4.5

My previous post was about how to create Custom Markup Extensions for XAML.  Until now, it was possible to use a markup extension in XAML to assign a value to a property, but couldn’t do the same to subscribe to an event. WPF 4.5 supports markup extensions for events. While WPF does not define a markup extension to be used for events, third parties are able to create a markup extension that can be used with events. When using the MVVM pattern, we often associates commands of the ViewModel with controls of the view, via binding mechanism. It has some drawbacks.Not all controls have Command property, and when exists, it corresponds to only one event of the control. The solution so far to this issue involved using the Interactivity assembly from the Expression Blend SDK. We can try something similar using the new feature, which will not require the Expression Blend SDK at all.

It is able to bind events directly to ViewModel methods, like this:

<Window x:Class="CustomMarkupForDateTime.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:custom="clr-namespace:CustomMarkupForDateTime"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <Button Width="300"
                Height="30"
                Click="{custom:CommandBinder ShowWindowCommand}" />
    </Grid>
</Window>

With the ShowWindow method defined in the ViewModel

 public class ViewModel
    {
        public ICommand ShowWindowCommand { get; set; }

        public ViewModel()
        {
            ShowWindowCommand = new DelegateCommand(ShowWindow);
        }
        private void ShowWindow(object obj)
        {
            System.Windows.MessageBox.Show("Hello World!");
        }
    }

CommandBinder Markup Extension :

using System;
using System.Windows;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Markup;
using System.Reflection;

namespace CustomMarkupForDateTime
{
    public class CommandBinder : MarkupExtension
    {
        private string _commandPath;
        private ICommand _command;
        private Type _eventArgsType;

        public CommandBinder(string commandPath)
        {
            _commandPath = commandPath;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            Delegate customDelegate = null;
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                EventInfo eventInfo = target.TargetProperty as EventInfo;
                MethodInfo methodInfo = GetType().GetMethod("InvokeCommand", BindingFlags.NonPublic | BindingFlags.Instance);
                Type parameterType = null;
                if (eventInfo != null)
                    parameterType = eventInfo.EventHandlerType;

                if (parameterType != null)
                {
                    _eventArgsType = parameterType.GetMethod("Invoke").GetParameters()[1].ParameterType;
                    customDelegate = Delegate.CreateDelegate(parameterType, this, methodInfo);
                }
            }
            return customDelegate;
        }
        private void InvokeCommand(object sender, RoutedEventArgs e)
        {
            var dataContext = (sender as FrameworkElement).DataContext;
            if (_commandPath != null)
            {
                _command = (ICommand)ParseCommandPath(dataContext, _commandPath);
            }
            var cmdParams = Activator.CreateInstance(_eventArgsType);
            if (_command != null && _command.CanExecute(cmdParams))
                _command.Execute(cmdParams);
        }
        private Object ParseCommandPath(object target, string commandPath)
        {
            return target.GetType().GetProperty(commandPath).GetValue(target);
        }
    }
}

Custom Markup Extensions in XAML

A markup extension can be implemented to provide values for properties in an attribute usage, properties in a property element usage, or both. When used to provide an attribute value, the syntax that distinguishes a markup extension sequence to a XAML processor is the presence of the opening and closing curly braces ({ and }). The type of markup extension is then identified by the string token immediately following the opening curly brace.  When used in property element syntax, a markup extension is visually the same as any other element used to provide a property element value: a XAML element declaration that references the markup extension class as an element, enclosed within angle brackets (<>).  [msdn]

For both the general XAML language and WPF specific markup extensions, the behavior of each markup extension is identified to a XAML processor through a class called MarkupExtension (System.Windows.Markup), and provides an implementation of the ProvideValue method. This method on each extension provides the object that is returned when the markup extension is evaluated. The returned object is typically evaluated based on the various string tokens that are passed to the markup extension.

There’s not much ceremony to creating a markup extension.

using System;
using System.Windows;
using System.Windows.Markup;

namespace CustomMarkupForDateTime
{
    public class DateMarkup : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return DateTime.Now.ToShortDateString();
         }
    }
}

<Window x:Class="CustomMarkupForDateTime.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:custom="clr-namespace:CustomMarkupForDateTime"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <TextBlock Text="{custom:DateMarkup}" />
    </Grid>
</Window>

Playing with the IserviceProvider

As the name suggest, it provides some services. Depending on the service you will call, you can have information about the types.  Here is the list of services provided.   Below example uses IProvideValueTarget service to get information about the target property name, type and the object owing that property.

using System;
using System.Windows;
using System.Windows.Markup;

namespace CustomMarkupForDateTime
{
    public class DateMarkup : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            string text = string.Format("Target Property Name : {1},{0} Type : {2}{0} Target Object Type : {3}",
                                        "\n", (service.TargetProperty as DependencyProperty).Name,
                                        (service.TargetProperty as DependencyProperty).PropertyType.Name,
                                        service.TargetObject.GetType().ToString());
            return text;
         }
    }
}

Markup Extension with Parameter

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Threading.Tasks;
using System.Windows.Markup;
using System.Reflection;
using System.Threading;

namespace CustomMarkupForDateTime
{
    public class DateMarkup : MarkupExtension
    {
        private DependencyProperty _property;
        private DependencyObject _object;

        [ConstructorArgument("DateTimeFormat")]
        public string DateTimeFormat { get; set; }

        public DateMarkup() { }
        public DateMarkup(string dateFormat)
        {
            DateTimeFormat = dateFormat;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            _property = service.TargetProperty as DependencyProperty;
            _object = service.TargetObject as DependencyObject;
            Timer _timer = new Timer(OnTimerCallback, this, 100, 100);            
            return DateTime.Now.ToString(DateTimeFormat);
        }
        private void OnTimerCallback(object state)
        {
            DateMarkup markup = (DateMarkup)state;
            if (!markup._object.Dispatcher.CheckAccess())
            {
                markup._object.Dispatcher.Invoke(() => markup._object.SetValue(_property, DateTime.Now.ToString(DateTimeFormat)));
            }
            else
            {
             markup._object.SetValue(_property, DateTime.Now.ToString(DateTimeFormat));
            }
        }
    }
}
<Window x:Class="CustomMarkupForDateTime.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:custom="clr-namespace:CustomMarkupForDateTime"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <TextBlock Text="{custom:DateMarkup DateTimeFormat=dd/MMM/yyyy-hh:mm:ss}" />
    </Grid>
</Window>