Tommaso Belluzzo
Tommaso Belluzzo

Reputation: 23685

Custom WPF/XAML Canvas

I'm trying to create and use a custom Canvas. Here is the XAML (MyCanvas.xaml):

<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:Core="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Namespace="clr-namespace:MyNamepace" xmlns:Properties="clr-namespace:MyNamepace.Properties" Core:Class="MyNamepace.MyCanvas">
    <Canvas.Resources>
        <Namespace:ImagesConverter Core:Key="ImagesConverter"/>
    </Canvas.Resources>
    <Image Source="{Binding Source={Core:Static Properties:Resources.Background}, Converter={StaticResource ImagesConverter}}" Stretch="Fill"/>
</Canvas>

Here is the code declaration (MyCanvas.xaml.cs):

public partial class MyCanvas : Canvas

When I try to use it like so:

<Namespace:MyCanvas Core:Name="Layout" Loaded="OnLoaded">
    <Namespace:MyUserControl Core:Name="Control1" Namespace:MyCanvas.Left="50" MyProperty="50">
        <Namespace:MyCanvas.Top>
            <MultiBinding Converter="{StaticResource MathConverter}" ConverterParameter="(x - y) / 2">
                <Binding ElementName="Layout" Path="ActualHeight"/>
                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
            </MultiBinding>
        </Namespace:MyCanvas.Top>
    </Namespace:MyUserControl>
    <Namespace:MyUserControl Core:Name="Control2" Namespace:MyCanvas.Left="744" Namespace:MyCanvas.Top="42" MyProperty="150"/>
</Namespace:MyCanvas>

I get two different errors:

The property "Content" can only be set once. ==> Isn't it inheriting Canvas?!?!?!

The member "Top" is not recognized or is not accessible. ==> Isn't it inheriting Canvas again?!?!?! The member "Left" is not recognized or is not accessible. ==> Isn't it inheriting Canvas again?!?!?!

EDIT: this is what I have so far... still getting the "Content" already set error!

MyCanvas.xaml

<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:Core="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Namespace="clr-namespace:MyNamespace" xmlns:Properties="clr-namespace:MyNamespace.Properties" Core:Class="MyNamespace.MyCanvas">
    <Canvas.Background>
        <ImageBrush ImageSource="{Binding Source={Core:Static Properties:Resources.Background}, Converter={StaticResource ImagesConverter}}" Stretch="Fill"/>
    </Canvas.Background>
    <Canvas.Resources>
        <Namespace:ImagesConverter Core:Key="ImagesConverter"/>
    </Canvas.Resources>
</Canvas>

MyCanvas.xaml.cs

public class MyCanvas : Canvas
{
    // ...
}

MainWindow.xaml

<Namespace:MyCanvas Core:Name="MyCanvas" Loaded="OnLoaded">
    <Namespace:MyUserControl ...
    <Namespace:MyUserControl ...
    <Namespace:MyUserControl ...
</Namespace:MyCanvas>

Upvotes: 3

Views: 9698

Answers (2)

sircodesalot
sircodesalot

Reputation: 11439

Adding to Daniel's answer, you have to understand that WPF introduces a concept called 'Dependency Properties' and 'Dependency Objects'. In short, just as Objects in C# can have properties, Dependency Objects in WPF can have dependency properties. The way this works is a bit different though that the c# method.

For dependency properties (such as attached properties), WPF manages a seperate table with records indicating which objects have which properties. Which in other words, means it's possible for any object to have any property, WPF just adds a record to the table giving said object said property. So for example, Canvas.Left, while being defined by Canvas, can be implemented by any control. WPF simply inserts a record into the dependency table, and voila now your image has Canvas.Left / Canvas.Top properties. This means a much smaller memory footprint, because object opt into having a property (when the record is added), rather than having a property just because it derives from a certain class.

The way this works programatically, is you use DependencyObject.SetValue / GetValue (all WPF objects derive from DependencyObject see: http://miteshsureja.blogspot.com/2011/06/wpf-class-hierarchy.html - in fact, memorize this graph because it will help you better to understand how WPF works under the hood), and this adds / reads from a record to the aforementioned table. When you define a dependency property, you your c# accessors should really redirect to these methods, because WPF (and not the object itself) should be managing these values (this is how WPF is able to do data-binding and what not, because it manages the values and just shifts them to other objects as neccesasry). For an example of creating a dependency property, see: http://msdn.microsoft.com/en-us/library/ms752914.aspx . The concept behind dependency properties is very simple, but you have to at least be aware of it in order to understand a lot of WPFs functionality.

Upvotes: 4

Daniel Hilgarth
Daniel Hilgarth

Reputation: 174457

Left and Top are attached properties. That means that they are not inherited by your class.

You need to change the user control declaration to use Canvas.Left and Canvas.Top instead:

<Namespace:MyUserControl Core:Name="Control2" Canvas.Left="744" Canvas.Top="42" 
                         MyProperty="150"/>

The problem with the content is that you set it twice, just like the error message says.

  1. In MyCanvas.xaml you set it to an Image.
  2. When using it, you set it to your user controls.

To fix it, you need to add an ItemsControl to MyCanvas and declare it as the control that represents the content:

<Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:Core="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Namespace="clr-namespace:MyNamepace" xmlns:Properties="clr-namespace:MyNamepace.Properties" Core:Class="MyNamepace.MyCanvas">
    <Canvas.Resources>
        <Namespace:ImagesConverter Core:Key="ImagesConverter"/>
    </Canvas.Resources>
    <Image Source="{Binding Source={Core:Static Properties:Resources.Background}, Converter={StaticResource ImagesConverter}}" Stretch="Fill"/>
    <ItemsControl Content="{Binding Path=LocalContent, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Namespace:MyCanvas}}}" />
</Canvas>

In your class file:

[ContentProperty("LocalContent")]
public partial class MyCanvas : Canvas
{
    public static readonly DependencyProperty LocalContentProperty =
        DependencyProperty.Register(
            "LocalContent", 
            typeof(UIElementCollection), 
            typeof(MyCanvas ), 
            new PropertyMetadata(default(UIElementCollection)));
}

Upvotes: 4

Related Questions