WPF --- Non-Button custom control realizes click function

introduction

Today I am working on a function to set the folder path, which is a text box, add a button, click the button, pop up and select the folder FolderBrowserDialogpath, a simple way, you can directly StackPanelplace one TextBoxand one horizontally, and then click the button to give Image Buttonin the background code assignment. But this is not elegant enough, the UI is not elegant enough, and the code implementation can be described as strong coupling. Then I will share my implementation plan.ViewModelFilePath

Target

To do this function of setting the folder path, my goal is to click anywhere to open it FolderBrowserDialog, then I need to use the text box and button as a whole control, and after selecting the folder path, I will assign a value ViewModelto the FilePathbound .

Preparation

First of all, since an overall control is to be designed, the UI is as follows:

image.png

Next, create this overall control, use it Buttondirectly without using it Control, to create a custom control OpenFolderBrowserControl:

Code Behind code is as follows:

public class OpenFolderBrowserControl : Control,
{
    static OpenFolderBrowserControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(OpenFolderBrowserControl), new FrameworkPropertyMetadata(typeof(OpenFolderBrowserControl)));
    }

    public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(OpenFolderBrowserControl));

    [Description("文件路径")]
    public string FilePath
    {
        get => (string)GetValue(FilePathProperty);
        set => SetValue(FilePathProperty, value);
    }
}

The design code in Themes/Generic.xaml is as follows:

<Style TargetType="{x:Type local:OpenFolderBrowserControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:OpenFolderBrowserControl}">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel Orientation="Horizontal">

                            <TextBox
                                Width="{TemplateBinding Width}"
                                Height="56"
                                Padding="0,0,60,0"
                                IsEnabled="False"
                                IsReadOnly="True"
                                Text="{Binding FilePath, RelativeSource={RelativeSource Mode=TemplatedParent}}">
                                <TextBox.Style>
                                    <Style TargetType="{x:Type TextBox}">
                                        <Setter Property="Background" Value="White" />
                                        <Setter Property="BorderBrush" Value="#CAD2DD" />
                                        <Setter Property="Foreground" Value="#313F56" />
                                        <Setter Property="BorderThickness" Value="2" />
                                        <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
                                        <Setter Property="HorizontalContentAlignment" Value="Left" />
                                        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                                        <Setter Property="AllowDrop" Value="False" />
                                        <Setter Property="FontSize" Value="22" />
                                        <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
                                        <Setter Property="Stylus.IsFlicksEnabled" Value="False" />
                                        <Setter Property="HorizontalAlignment" Value="Left" />
                                        <Setter Property="VerticalAlignment" Value="Center" />
                                        <Setter Property="Margin" Value="20,0,0,0" />
                                        <Setter Property="Template">
                                            <Setter.Value>
                                                <ControlTemplate TargetType="{x:Type TextBox}">
                                                    <Border
                                                        x:Name="border"
                                                        Background="{TemplateBinding Background}"
                                                        BorderBrush="{TemplateBinding BorderBrush}"
                                                        BorderThickness="{TemplateBinding BorderThickness}"
                                                        CornerRadius="8"
                                                        SnapsToDevicePixels="True">
                                                        <Grid>
                                                            <ScrollViewer
                                                                x:Name="PART_ContentHost"
                                                                Margin="20,0,0,0"
                                                                VerticalAlignment="{TemplateBinding VerticalAlignment}"
                                                                VerticalContentAlignment="Center"
                                                                Focusable="False"
                                                                FontFamily="{TemplateBinding FontFamily}"
                                                                FontSize="{TemplateBinding FontSize}"
                                                                HorizontalScrollBarVisibility="Hidden"
                                                                VerticalScrollBarVisibility="Hidden" />
                                                            <TextBlock
                                                                x:Name="WARKTEXT"
                                                                Margin="20,0,0,0"
                                                                HorizontalAlignment="Left"
                                                                VerticalAlignment="Center"
                                                                FontFamily="{TemplateBinding FontFamily}"
                                                                FontSize="{TemplateBinding FontSize}"
                                                                Foreground="#A0ADBE"
                                                                Text="{TemplateBinding Tag}"
                                                                Visibility="Collapsed" />
                                                        </Grid>
                                                    </Border>
                                                    <ControlTemplate.Triggers>
                                                        <Trigger Property="IsEnabled" Value="False">
                                                            <Setter TargetName="border" Property="Opacity" Value="0.56" />
                                                        </Trigger>
                                                        <Trigger Property="IsMouseOver" Value="True">
                                                            <Setter TargetName="border" Property="BorderBrush" Value="#CAD2DD" />
                                                        </Trigger>
                                                        <Trigger Property="IsKeyboardFocused" Value="True">
                                                            <Setter TargetName="border" Property="BorderBrush" Value="#CAD2DD" />
                                                        </Trigger>
                                                        <MultiTrigger>
                                                            <MultiTrigger.Conditions>
                                                                <Condition Property="Text" Value="" />
                                                                <!--<Condition Property="IsFocused" Value="False"/>-->
                                                            </MultiTrigger.Conditions>
                                                            <Setter TargetName="WARKTEXT" Property="Visibility" Value="Visible" />
                                                        </MultiTrigger>
                                                    </ControlTemplate.Triggers>
                                                </ControlTemplate>
                                            </Setter.Value>
                                        </Setter>
                                    </Style>
                                </TextBox.Style>
                            </TextBox>
                            <Border
                                Height="56"
                                Margin="-60,0,0,0"
                                Background="White"
                                BorderBrush="#CAD2DD"
                                BorderThickness="2"
                                CornerRadius="0,8,8,0">
                                <StackPanel
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center"
                                    Orientation="Horizontal">
                                    <Ellipse
                                        Width="5"
                                        Height="5"
                                        Margin="3"
                                        Fill="#949494" />
                                    <Ellipse
                                        Width="5"
                                        Height="5"
                                        Margin="3"
                                        Fill="#949494" />
                                    <Ellipse
                                        Width="5"
                                        Height="5"
                                        Margin="3"
                                        Fill="#949494" />
                                </StackPanel>
                            </Border>

                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

The controls created in this way actually have no click function.

Then let's take a look at the implementation of the click function solution.

Click to realize the function plan

Because of the existence of MVVMButton , there are two options for the click function in WPF ,

  • The first is to directly register the click event, such asClick="OpenFolderBrowserControl_Click"
  • The second is to bind Command, CommandParameter, CommandTarget, for example Command="{Binding ClickCommand}" CommandParameter="" CommandTarget="" .

But what we defined above is one Control, which has neither Clicknor Command, so we need to OpenFolderBrowserControldefine Clickand for Command.

Define click event

It is relatively simple to define a click event, just declare one RoutedEventHandlerand name it as Click.

public event RoutedEventHandler? Click;

Define Command

The definition Commandrequires ICommandSourcethe interface, and the focus is on ICommandSourcethe interface.

ICommandSourceInterfaces are used to indicate that a control can generate and execute commands. The interface defines three members

  • defines a ICommandproperty of type Command,
  • Defines a representation associated with the control, IInputElementtypeCommandTarget
  • Defines an attribute representing the command parameter, objecttypeCommandParameter

The above two paragraphs are defined as follows:

public class OpenFolderBrowserControl : Control, ICommandSource
{
    //上文中已有代码此处省略...

    #region 定义点击事件

    public event RoutedEventHandler? Click;

    #endregion


    #region 定义command

    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICommand), typeof(OpenFolderBrowserControl), new UIPropertyMetadata(null))
    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }
    public object CommandParameter
    {
        get { return (object)GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    public static readonly DependencyProperty CommandParameterProperty =
        DependencyProperty.Register("CommandParameter", typeof(object), typeof(OpenFolderBrowserControl));

    public IInputElement CommandTarget
    {
        get { return (IInputElement)GetValue(CommandTargetProperty); }
        set { SetValue(CommandTargetProperty, value); }
    }

    public static readonly DependencyProperty CommandTargetProperty =
        DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(OpenFolderBrowserControl));

Implement the click function

Well, so far I have only defined the click event and Command, but there is no place where these two functions can be triggered.

Since the click function is to be realized, the most intuitive method is that this method is a virtual method of OnMouseLeftButtonUpthe WPF core base class , and we can directly rewrite it. UIElementThe following code:

public class OpenFolderBrowserControl : Control, ICommandSource
{
    //上文中已有代码此处省略...
    
    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {

        base.OnMouseLeftButtonUp(e);
        //调用点击事件
        Click?.Invoke(e.Source, e);
        //调用Command
        ICommand command = Command;
        object parameter = CommandParameter;
        IInputElement target = CommandTarget;

        RoutedCommand routedCmd = command as RoutedCommand;
        if (routedCmd != null && routedCmd.CanExecute(parameter, target))
        {
            routedCmd.Execute(parameter, target);
        }
        else if (command != null && command.CanExecute(parameter))
        {
            command.Execute(parameter);
        }
    }
}

At this point, our non-Button custom control needs to realize the click is completed, and then test it.

test

Prepare to test the form and ViewModel, here, in order not to introduce dependent packages, it can be regarded as a review of the implementation of MVVMICommand , and the and are implemented manually INotifyPropertyChanged.

ICommand implementation:

public class RelayCommand : ICommand
{
    private readonly Action? _execute;

    public RelayCommand(Action? execute)
    {
        _execute = execute;
    }

    public bool CanExecute(object? parameter)
    {
        return true;
    }

    public void Execute(object? parameter)
    {
        _execute?.Invoke();
    }

    public event EventHandler? CanExecuteChanged;
}

TestViewModel implementation: After the trigger
here ClickCommand, I output the current FilePathvalue.

public class TestViewModel : INotifyPropertyChanged
{

    public TestViewModel()
    {
        FilePath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string filePath = string.Empty;
    /// <summary>
    /// 文件路径
    /// </summary>
    public string FilePath
    {
        get { return filePath; }
        set { filePath = value; OnPropertyChanged(nameof(FilePath)); }
    }


    private ICommand clickCommand = null;
    /// <summary>
    /// 点击事件
    /// </summary>
    public ICommand ClickCommand
    {
        get { return clickCommand ??= new RelayCommand(Click); }
        set { clickCommand = value; }
    }

    private void Click()
    {
        MessageBox.Show($"ViewModel Clicked!The value of FilePath is {FilePath}");
    }
}

Form UI code :

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    
    <TextBlock
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        FontSize="22"
        Text="设置文件路径:" />

    <local:OpenFolderBrowserControl
        Grid.Column="1"
        HorizontalAlignment="Left"
        Click="OpenFolderBrowserControl_Click"
        Command="{Binding ClickCommand}"
        FilePath="{Binding FilePath, Mode=TwoWay}" />
</Grid>

Form Code Behind Code

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new TestViewModel();
    }

    private void OpenFolderBrowserControl_Click(object sender, RoutedEventArgs e)
    {
        FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();

        DialogResult result = folderBrowserDialog.ShowDialog();

        if (result == System.Windows.Forms.DialogResult.OK)
        {
            string selectedFolderPath = folderBrowserDialog.SelectedPath;

            var Target = sender as OpenFolderBrowserControl;

            if (Target != null)
            {
                Target.FilePath = selectedFolderPath;
            }
        }
    }
}

Test Results

I click anywhere throughout the control and it opens the folder browser.

image.png

After selecting the music folder, a pop-up window promptsViewModel Clicked!The value of FilePath is C:\Users\Administrator\Music

image.png

in conclusion

It can be seen from the test results that both Click and Command registered in the UI are triggered. This solution is just an introduction, as long as any control (non-button) needs to implement the click function, it can be implemented in this way.

The implementation core consists of two solutions:

  • Define the click event directly.
  • Implement ICommandSource.

Then rewrite various mouse events, such as mouse down, mouse up, double click, etc. can be realized.

The above solution not only guarantees the elegance of the UI but also ensures the separation of the front and back of the MVVM architecture.

If you have a better and more elegant solution, please leave a message to discuss.

Guess you like

Origin blog.csdn.net/lwf3115841/article/details/132363938