Question

I have created two user controls, one displays a 2d cartesian map of my data/view model collection on a Canvas. The other one is a 1-column scrollable image gallery, which displays an image for each data model. The (snippetized) map is based on:

<ItemsControl 
        ItemsSource="{Binding Defects}"
        ItemTemplateSelector="{StaticResource IconSelector}">            
        <!-- ItemsPanelTemplate -->
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas x:Name="MapCanvas" Height="10000"/>                       
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <!-- ItemContainerStyle -->
        <ItemsControl.ItemContainerStyle>
            <Style>
                <Setter Property="Canvas.Top" Value="{Binding Y}" />
                <Setter Property="Canvas.Left" Value="{Binding X}" />                     
            </Style>               
        </ItemsControl.ItemContainerStyle>
</ItemsControl>

The (again, snippetized) image gallery is based on:

<ScrollViewer Name="ScrollContainer"
        Grid.Column="1" PanningMode="VerticalOnly" 
        VerticalScrollBarVisibility="Visible"
        HorizontalScrollBarVisibility="Visible"                  
        Background="White">
    <ItemsControl Name="ItemsContainer" ItemsSource="{Binding Defects}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border BorderThickness="5" BorderBrush="Gray" Margin="2">
                    <Image Source="{Binding ImageUrl}" Margin="2"></Image>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

Now I want to add some synchronization to these two views of my data. Namely, when one of the icons on the map is clicked, I want the gallery to scroll to the corresponding image. I have tried to add a dependency property to both controls to point to the selected index. This is not really what I want though, as I want some extra flexibility. Sometimes I want the scrolling to be done with an animation, sometimes I want immediate synchronization of selected indices. Because of this, I have tried adding a custom command, BringIntoView, to the viewmodel. This command takes a bool input to specify whether the operation has to be performed with animation or not. I can bind the datatemplate to my custom command, and the map control correctly calls the execute on the custom command whenever an icon on the map is clicked.

But I am stuck on the gallery side, how can I bind this command to some code behind method, or maybe to some other method on the control? Command binding does not work, as it does not allow dynamic binding to the command. The following raises a compile time error, as the Command property on the CommandBinding is not a dependency property:

<UserControl.CommandBindings>
    <CommandBinding Command="{Binding BringIntoView}" Executed="OnBringIntoViewExecuted"/>
</UserControl.CommandBindings> 

I can add a static command and reference directly that one, but this would at least tie the view to the viewmodel command. So how do I achieve this level of command linking, at the same time preserving the decoupling from the view?

In other words, binding two UI-specific properties of two UI controls is allowed by the WPF dependency property system. How can I achieve the same for commands/events? That is, a UI event/command from a user control is connected to a method/command of another UI control?

Was it helpful?

Solution

I found a solution to this design issue. Similarly to what happens with dependency properties, a custom control can also expose custom commands. Since commands exposed directly by a control (as opposed to commands exposed by a viewmodel) are UI-specific, it's most appropriate to use RoutedUiCommand rather than custom ICommand implementations. RoutedUiCommands can be exposed as static properties, quite like what happens with DependencyProperty. In my case, the gallery control exposes a static BringIntoView command as such:

public partial class DefectGallery {
  // the control exposes the following commands:
  public static readonly RoutedUiCommand BringIntoView = new RoutedUiCommand;
  // the control exposes the following depdendency properties:
  public static readonly DependencyProperty ScrollSpeedProperty = DependencyProperty.Register(...);   
  ... 
}  

The command is bound to code-behind from the xaml file of the DefectGallery control:

<UserControl.CommandBindings>
   <CommandBinding Command="{x:static local:DefectGallery.BringIntoView}" Executed="OnBringIntoViewExecuted"/>
</UserControl.CommandBindings> 

Finally, some other control needs to trigger the BringIntoViewCommand. It could e.g. a WPF provided command source like a button. In my case, I have implemented the ICommandSource interface on the map control in order in order to allow specifying a custom command that is executed whenever an icon on the map is clicked. I can now use the map control (first snippet above) from xaml like this:

<MapControl 
    Command="{x:static local:DefectGallery.BringIntoView}"
    CommandTarget="{Binding ElementName=defectGalleryInstance}"
 />

Thus effectively I can "call" a control from another control using just declarative xaml.

Now, this seems to be as basic a pattern as adding a dependency property, so I am just surprised I have not found guidelines on this. It seems natural to me that a control exposes properties as well as commands; I consider these two to be the "interface" of the UI control towards other UI components, whereas the View model bindings are the interface towards the application layer. Oh well, WPF lends itself to different usage approaches, you just have to find what works for you/your application.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top