How do I implement a DependencyProperty on a custo

2019-08-13 04:03发布

问题:

How do I implement a DependencyProperty on a custom wpf control for an ImageSource?

I created a custom control (button) which amongst other things does display an image. I want to be able to set the ImageSource for the image from the outside of the control, so I implemented a DependencyProperty. Whenever I try to change the ImageSource though, I am getting a SystemInvalidOperationException: "The calling thread cannot access this object because a different thread owns it."

OK, so the main thread cannot access the image control, so I need to use the Dispatcher - but where and how? Apparently the exception is thrown in the setter, executing SetValue(ImageProperty, value);

tv_CallStart.xaml:

<Button x:Class="EHS_TAPI_Client.Controls.tv_CallStart" x:Name="CallButton"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="24" d:DesignWidth="24" Padding="0" BorderThickness="0"
         Background="#00000000" BorderBrush="#FF2467FF" UseLayoutRounding="True">

         <Image x:Name="myImage"
                Source="{Binding ElementName=CallButton, Path=CallImage}" />
</Button>

tv_CallStart.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace EHS_TAPI_Client.Controls
{
    public partial class InitiateCallButton : Button
    {
        public ImageSource CallImage
        {
            get { return (ImageSource)GetValue(CallImageProperty); }
            set { SetValue(CallImageProperty, value); }
        }
        public static readonly DependencyProperty CallImageProperty =
            DependencyProperty.Register("CallImage", typeof(ImageSource), typeof(InitiateCallButton), new UIPropertyMetadata(null));

        public InitiateCallButton()
        {
            InitializeComponent();
        }
    }
}

setting the image from the code-behind of the UI-thread:

BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri("pack://application:,,,/EHS-TAPI-Client;component/Images/call-start.png");
bi.EndInit();
bi.Freeze();
CallButton.CallImage = bi;

and the MainWindow.xaml where the control is initialised:

<ctl:InitiateCallButton x:Name="CallButton" CallImage="/Images/call-start.png" />

Adapted the above source code to reflect my progress..

Solution:

The code posted above is working fine. The important change from the initial version is the addition of the Freeze() method from the UI thread (see accepted answer). The actual problem in my project is not the button not being initialised in the UI thread, but the new image being set from another thread. The image is set in an event handler which itself is triggered from another thread. I resolved the problem by using the Dispatcher:

BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.UriSource = new Uri("pack://application:,,,/EHS-TAPI-Client;component/Images/call-start.png");
bi.EndInit();
bi.Freeze();
if (!Application.Current.Dispatcher.CheckAccess())
{
    Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate()
        {
            CallButton.CallImage = bi;
        });
}
else
{
    CallButton.CallImage = bi;
}

回答1:

  1. If you use an image internally it makes sense to reuse the Image.SourceProperty and not that of ImageBrush.

  2. Make sure to create all thread-affine elements on the UI-thread or you need to Freeze them if they are freezable before you do anything cross-thread with them. (ImageSource is a Freezable)

  3. The property changed handler is not hooked up so it will never be called, its content is pointless anyway though, the property has already been set when the handler is called.