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 FolderBrowserDialog
path, a simple way, you can directly StackPanel
place one TextBox
and one horizontally, and then click the button to give Image Button
in 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.ViewModel
FilePath
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 ViewModel
to the FilePath
bound .
Preparation
First of all, since an overall control is to be designed, the UI is as follows:
Next, create this overall control, use it Button
directly 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 as
Click="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 Click
nor Command
, so we need to OpenFolderBrowserControl
define Click
and for Command
.
Define click event
It is relatively simple to define a click event, just declare one RoutedEventHandler
and name it as Click
.
public event RoutedEventHandler? Click;
Define Command
The definition Command
requires ICommandSource
the interface, and the focus is on ICommandSource
the interface.
ICommandSource
Interfaces are used to indicate that a control can generate and execute commands. The interface defines three members
- defines a
ICommand
property of typeCommand
, - Defines a representation associated with the control,
IInputElement
typeCommandTarget
- Defines an attribute representing the command parameter,
object
typeCommandParameter
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 OnMouseLeftButtonUp
the WPF core base class , and we can directly rewrite it. UIElement
The 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 FilePath
value.
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.
After selecting the music folder, a pop-up window promptsViewModel Clicked!The value of FilePath is C:\Users\Administrator\Music
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.