tags:

views:

87

answers:

1

I have the following class:

public class LooklessControl : Control
{
    public List<int> IntList { get; private set; }
    public int CurrentInt { get; private set; }

    private int _index = 0;

    static LooklessControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LooklessControl), new FrameworkPropertyMetadata(typeof(LooklessControl)));
    }

    public LooklessControl()
    {
        IntList = new List<int>();
        for (int i = 0; i < 10; i++)
        {
            IntList.Add(i);
        }
        CurrentInt = IntList[_index];
    }

    public static readonly RoutedCommand NextItemCommand =
        new RoutedCommand("NextItemCommand", typeof(LooklessControl));

    private void ExecutedNextItemCommand(object sender, ExecutedRoutedEventArgs e)
    {
        NextItemHandler();
    }

    private void CanExecuteNextItemCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
    }

    public static readonly RoutedCommand PrevItemCommand =
        new RoutedCommand("PrevItemCommand", typeof(LooklessControl));

    private void ExecutedPrevItemCommand(ExecutedRoutedEventArgs e)
    {
        PrevItemHandler();
    }

    private void CanExecutePrevItemCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
    }


    public static readonly RoutedEvent NextItemEvent =
        EventManager.RegisterRoutedEvent("NextItemEvent", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(LooklessControl));

    public event RoutedEventHandler NextItem
    {
        add { AddHandler(NextItemEvent, value); }
        remove { RemoveHandler(NextItemEvent, value); }
    }

    private void RaiseNextItemEvent()
    {
        RoutedEventArgs args = new RoutedEventArgs(LooklessControl.NextItemEvent);
        RaiseEvent(args);
    }

    public static readonly RoutedEvent PrevItemEvent =
        EventManager.RegisterRoutedEvent("PrevItemEvent", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(LooklessControl));

    public event RoutedEventHandler PrevItem
    {
        add { AddHandler(PrevItemEvent, value); }
        remove { RemoveHandler(PrevItemEvent, value); }
    }

    private void RaisePrevItemEvent()
    {
        RoutedEventArgs args = new RoutedEventArgs(LooklessControl.PrevItemEvent);
        RaiseEvent(args);
    }

    private void NextItemHandler()
    {
        _index++;
        if (_index == IntList.Count)
        {
            _index = 0;
        }

        CurrentInt = IntList[_index];
        RaiseNextItemEvent();
    }

    private void PrevItemHandler()
    {
        _index--;
        if (_index == 0)
        {
            _index = IntList.Count - 1;
        }

        CurrentInt = IntList[_index];
        RaisePrevItemEvent();
    }
}

The class has a default style, in Generic.xaml, that looks like this:

<Style x:Key="{x:Type local:LooklessControl}" TargetType="{x:Type local:LooklessControl}">
    <Setter Property="Height" Value="200"/>
    <Setter Property="Width" Value="90"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:LooklessControl}">
                <Border BorderBrush="Black" BorderThickness="1" Padding="2">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="20"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Rectangle Grid.Row="0" Fill="LightGray"/>
                        <Rectangle Grid.Row="1" Fill="Gainsboro"/>
                        <Grid Grid.Row="0">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="10"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="10"/>
                            </Grid.ColumnDefinitions>
                            <Path Grid.Column="0" x:Name="pathLeftArrow" Data="M0,0.5 L1,1 1,0Z" Width="6" Height="14" Stretch="Fill"
                                  HorizontalAlignment="Center" Fill="SlateBlue"/>
                            <TextBlock Grid.Column="1" Name="textBlock" 
                                   Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=CurrentInt}" 
                                   HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="Junction" FontSize="13"/>
                            <Path Grid.Column="2" x:Name="pathRightArrow" Data="M0,0 L1,0.5 0,1Z" Width="6" Height="14" Stretch="Fill"
                                  HorizontalAlignment="Center" Fill="SlateBlue"/>
                        </Grid>
                        <ListBox Grid.Row="1" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Background="Transparent" 
                             ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IntList}"/>
                    </Grid>
                </Border>                    
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

How do I make it so that when the user clicks on pathLeftArrow it fires LooklessControl.PrevItemCommand, or or they click on pathRightArrow and it fires LooklessControl.NextItemCommand, or they click on an item in the ListBox and LooklessControl is notified of the newly selected item?

In other words, without adding x:Class to the top of Generic.xaml and thus creating a code-behind file for it, which I assume you wouldn't want to do, how do you handle events for elements in your xaml that don't have a Command property (which is just about everything other than a Button)?

Should LooklessControl have it's own XAML file (much like what you get when you create a new UserControl) associated with it that Generic.xaml just pulls in as a MergedDictionar as its default template? Or is there some other acknowledged way to do what I'm trying to do?

+2  A: 

To answer your last question: NO. The lookless control shouldn't require any known XAML. That is what lookless means.

You have a couple of options here, but I would recommend wrapping your elements in Buttons with a basically empty control template:

<ControlTemplate x:Key="contentOnlyButton" TargetType="{x:Type Button}">
    <ContentPresenter />
</ControlTemplate>

...

   <Button Grid.Column="0" Template="{StaticResource contentOnlyButton}"
           Command="{x:Static local:LooklessControl.PrevItemCommand}">
       <Path x:Name="pathLeftArrow" Data="M0,0.5 L1,1 1,0Z" Width="6" Height="14" 
             Stretch="Fill" HorizontalAlignment="Center" Fill="SlateBlue"/>
   </Button>

Your other option (and I would say this is probably not what you should do for executing commands on clicks, but may be applicable in other circumstances), would be to look for the named part in your template in OnApplyTemplate, and wire up the events.

public override void OnApplyTemplate()
{
    var prevElement = this.GetTemplateChild("PART_PathLeftArrow") as UIElement;
    if (prevElement != null)
        prevElement.MouseDown += (o, e) => PrevItemHandler();
    ...
}

One thing to note with doing this is that the Template isn't required to define the parts you are looking for, so you need to gracefully check for that circumstance. Throwing NullReferenceExceptions here will make restyling your control a royal pain for designers / developers who accidentally delete a required element. You will also want to follow the standard practice of naming your required elements with a PART_ syntax, and decorating your class with TemplatePart attributes.

[TemplatePart(Name = "PART_PathLeftArrow", Type = typeof(UIElement))]
[TemplatePart(Name = "PART_PathRightArrow", Type = typeof(UIElement))]
...
public class LooklessControl : Control

Edit: In order for the Button's to respond to the clicks, you need to setup CommandBindings to your functions that you had already defined. You would do this as a class command binding like so:

static LooklessControl()
{
    CommandManager.RegisterClassCommandBinding(
        typeof(LooklessControl),
        new CommandBinding(NextItemCommand, ExecutedNextItemCommand, CanExecuteNextItemCommand));

    CommandManager.RegisterClassCommandBinding(
        typeof(LooklessControl),
        new CommandBinding(PrevItemCommand, ExecutedPrevItemCommand, CanExecutePrevItemCommand));
}

The reason to do a class command binding is that if you add it to your control's CommandBindings collection, somebody using your control could inadvertently remove them. Also remember to update your command handling methods to have static semantics.

Abe Heidebrecht
+1 for buttons. This is also how ScrollBar does it. See http://www.codeproject.com/Articles/37366/Styling-A-ScrollViewer-Scrollbar-In-WPF.aspx
Lars Truijens
I agree, the buttons idea seems the best route. When I hooked them up as recommended above though, I get a button that no longer clicks. When I remove the Command property, the buttons click (but obviously do nothing) and when I put the Command property back in like so: Command="{x:Static local:LooklessControl.PrevItemCommand}"they become flat buttons that don't click and don't fire off the command.So what incredibly simple thing am I not doing correctly? Any ideas?
Scott
I updated the answer to include adding CommandBindings in the control.
Abe Heidebrecht
Yep, I knew it was something simple. I'm not that surprised that I forgot to include the CommandBindings, but I am surprised that I failed to notice that I'd forgotten to include them after looking at that code as many times as I did. Thanks for all of your help.
Scott