Wpf UserControl with its own data context and exte

2019-09-17 14:59发布

问题:

I'm trying to create a simple AudioPlayer control multiple reuse in a solution I'm working on. I have seen numerous example in various posts and blogs around the net and from those have created a small control with four buttons.

The xaml is defined thus:

<UserControl x:Class="AudioPlayer"
         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="30" d:DesignWidth="150">
<StackPanel Orientation="Horizontal">
    <StackPanel.Resources>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Margin" Value="10,0,0,0" />
        </Style>
    </StackPanel.Resources>
     <MediaElement Name="media" Source="{Binding Source}" LoadedBehavior="{Binding LoadedBehavior}"/>
    <Button Width="24" Height="24" x:Name="Repeat" Background="Transparent" BorderBrush="Transparent">
        <Image Source="Images/button_blue_repeat.png" ToolTip="Repeat"/>
    </Button>
    <Button Width="24" Height="24" x:Name="Play" Background="Transparent" BorderBrush="Transparent">
        <Image Source="Images/button_blue_play.png" ToolTip="Play"/>
    </Button>
    <Button Width="24" Height="24" x:Name="Pause" Background="Transparent" BorderBrush="Transparent">
        <Image Source="Images/button_blue_pause.png" ToolTip="Pause"/>
    </Button>
    <Button Width="24" Height="24" x:Name="Stop" Background="Transparent" BorderBrush="Transparent">
        <Image Source="Images/button_blue_stop.png" ToolTip="Stop"/>
    </Button>
</StackPanel>

With fairly simple code in the background;

Public Class AudioPlayer

Public Sub New()

    InitializeComponent()
    DataContext = New AudioPlayerViewModel With {.MediaElement = media, .Source = "bag1.mp3", .LoadedBehavior = MediaState.Manual, .CanCommandExecute = True}

End Sub

End Class

    Public Class AudioPlayerViewModel
        Inherits DependencyObject

        Public Sub New()
            Me.MediaCommand = New MediaElementCommand(Me)
        End Sub
        Public Property MediaElement() As MediaElement
        Public Property Source() As String
        Public Property LoadedBehavior() As MediaState
        Public Property CanCommandExecute() As Boolean
        Public Property MediaCommand() As ICommand
    End Class

Public Class MediaElementCommand
    Implements ICommand

    Private vm As AudioPlayerViewModel
    Public Sub New(ByVal vm As AudioPlayerViewModel)
        Me.vm = vm
    End Sub
    Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
        Return vm.CanCommandExecute
    End Function
    Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
        AddHandler(ByVal value As EventHandler)
            AddHandler CommandManager.RequerySuggested, value
        End AddHandler
        RemoveHandler(ByVal value As EventHandler)
            RemoveHandler CommandManager.RequerySuggested, value
        End RemoveHandler
        RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
        End RaiseEvent
    End Event
    Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
        Dim action As String = DirectCast(parameter, String)
        Select Case action.ToLower()
            Case "play"
                vm.MediaElement.Position = TimeSpan.Zero
                vm.MediaElement.Play()
            Case "stop"
                vm.MediaElement.Stop()
            Case "pause"
                vm.MediaElement.Pause()
            Case "resume"
                vm.MediaElement.Play()
            Case Else
                Throw New NotSupportedException(String.Format("Unknown media action {0}", action))
        End Select
    End Sub
End Class

My question quite simply is this. From the code you can see that at present the sound that is being played is hard coded. What I would like to know is wheteher it would be possible to create a dependency property for this control (I presume it would be of type string to represent a path to a sound file but I'm not sure) so that when the control is created in other controls or windows their viewmodels can pass a sound property to it (if that makes sense!). If it is possible where should I create it in respect of the code snippets shown?

Many thanks

回答1:

You could create a DP, but it would not work the way users would expect.

For example, if the user were to write

<local:AudioPlayer Media="{Binding SomeString}" />

Then WPF tries to set Media = DataContext.SomeString

But since you have hardcoded DataContext = New AudioPlayerViewModel in the constructor, then the binding will most likely fail because users will be expecting their inherited DataContext to be used by the UserControl, but the hardcoded DataContext will be used instead.


It is always my advice to never hardcode the DataContext property inside of a UserControl. It breaks the entire WPF design pattern of having separate layers for UI and Data.

Either build a UserControl specifically for use with a specific Model or ViewModel being used as the DataContext, such as this :

<!-- Draw anything of type AudioPlayerViewModel with control AudioPlayer -->
<!-- DataContext will automatically set to the AudioPlayerViewModel -->
<DataTemplate DataType="{x:Type local:AudioPlayerViewModel}}">
    <local:AudioPlayer /> 
</DataTemplate>

Or build it with the expectation that the DataContext can be absolutely anything, and DependencyProperites will be used to give the control the data it needs :

<!-- DataContext property can be anything, as long as it as the property MyString -->
<local:AudioPlayer Media="{Binding MyString}" />

The easiest way to get your code to work would probably be

  • Create the ViewModel as a private property instead of assiging it to the UserControl.DataContext
  • Bind or set the DataContext of the top level child inside your UserControl to your private property (in your case, the StackPanel)
  • Adjust the binding for your MediaElement to read from a custom DependencyProperty instead of from StackPanel.DataContext

Something like this :

<UserControl x:Name="MyAudioPlayer" ...>
    <StackPanel x:Name="AudioPlayerRoot">
        ...
        <MediaElement Source="{Binding ElementName=MyAudioPlayer, Path=MediaDependecyProperty}" ... />
        ...
    </StackPanel>
</UserControl>
Public Sub New()
    InitializeComponent()
    AudioPlayerRoot.DataContext = New AudioPlayerViewModel ...
End Sub