XAML WinUI 3:为什么为自定义控件定义的可视状态不起作用?

iq0todco  于 2023-03-16  发布在  其他
关注(0)|答案(1)|浏览(163)

我试图在WinUI 3上做一个自定义控件,它可以验证用户输入并根据结果对控件进行更改。

通用.xaml:

<Style TargetType="controls:InputValidationHost">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:InputValidationHost">
                <StackPanel>
                    <ContentPresenter Content="{TemplateBinding InputControl}" x:Name="InputControl" />
                    <ContentPresenter x:Name="CaptionText"
                        FontSize="{StaticResource CaptionTextFontSize}" />

                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="SetStyleByState">
                            <VisualState x:Name="Unvalidated">
                                <VisualState.StateTriggers>
                                    <StateTrigger IsActive="{TemplateBinding IsUnvalidated}" />
                                </VisualState.StateTriggers>
                                <VisualState.Setters>
                                    <Setter Target="CaptionText.Content"
                                        Value="{TemplateBinding UnvalidatedMessage}" />
                                    <Setter Target="CaptionText.Foreground"
                                        Value="{ThemeResource SystemColorGrayTextColor}" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Passed">
                                <VisualState.StateTriggers>
                                    <StateTrigger IsActive="{TemplateBinding HasPassed}" />
                                </VisualState.StateTriggers>
                                <VisualState.Setters>
                                    <Setter Target="CaptionText.Content"
                                        Value="{TemplateBinding PassedMessage}" />
                                    <Setter Target="CaptionText.Foreground" Value="Green" />
                                    <Setter Target="InputControl.Content.BorderBrush" Value="Green" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="Error">
                                <VisualState.StateTriggers>
                                    <StateTrigger IsActive="{TemplateBinding HasError}" />
                                </VisualState.StateTriggers>
                                <VisualState.Setters>
                                    <Setter Target="CaptionText.Content"
                                        Value="{TemplateBinding ErrorMessage}" />
                                    <Setter Target="CaptionText.Foreground" Value="Red" />
                                    <Setter Target="InputControl.Content.BorderBrush" Value="Red" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

输入验证主机.cs:

// Copyright (c) Microsoft Corporation and Contributors.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using AkiraVoid.WordBook.Enums;
using AkiraVoid.WordBook.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace AkiraVoid.WordBook.Controls
{
    [ContentProperty(Name = "InputControl")]
    public sealed class InputValidationHost : Control
    {
        public InputValidationHost()
        {
            this.DefaultStyleKey = typeof(InputValidationHost);
        }

        public Control InputControl
        {
            get => (Control)GetValue(InputControlProperty);
            set => SetValue(InputControlProperty, value);
        }

        public static readonly DependencyProperty InputControlProperty = DependencyProperty.Register(
            nameof(InputControl),
            typeof(Control),
            typeof(InputValidationHost),
            new(null));

        public InputValidationState State
        {
            get => (InputValidationState)GetValue(StateProperty);
            set => SetValue(StateProperty, value);
        }

        public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
            nameof(State),
            typeof(InputValidationState),
            typeof(InputValidationHost),
            new(InputValidationState.Unvalidated, StateChangedCallback));

        public IList<Func<object, bool>> Validators
        {
            get => (IList<Func<object, bool>>)GetValue(ValidatorsProperty);
            set => SetValue(ValidatorsProperty, value);
        }

        public static readonly DependencyProperty ValidatorsProperty = DependencyProperty.Register(
            nameof(Validators),
            typeof(IList<Func<object, bool>>),
            typeof(InputValidationHost),
            new(new List<Func<object, bool>>()));

        public object ErrorMessage
        {
            get => GetValue(ErrorMessageProperty);
            set => SetValue(ErrorMessageProperty, value);
        }

        public static readonly DependencyProperty ErrorMessageProperty = DependencyProperty.Register(
            nameof(ErrorMessage),
            typeof(object),
            typeof(InputValidationHost),
            new(null));

        public object PassedMessage
        {
            get => GetValue(PassedMessageProperty);
            set => SetValue(PassedMessageProperty, value);
        }

        public static readonly DependencyProperty PassedMessageProperty = DependencyProperty.Register(
            nameof(PassedMessage),
            typeof(object),
            typeof(InputValidationHost),
            new(null));

        public object UnvalidatedMessage
        {
            get => GetValue(UnvalidatedMessageProperty);
            set => SetValue(UnvalidatedMessageProperty, value);
        }

        public static readonly DependencyProperty UnvalidatedMessageProperty = DependencyProperty.Register(
            nameof(UnvalidatedMessage),
            typeof(object),
            typeof(InputValidationHost),
            new(null));

        public bool HasPassed
        {
            get => (bool)GetValue(HasPassedProperty);
            set => SetValue(HasPassedProperty, value);
        }

        public static readonly DependencyProperty HasPassedProperty = DependencyProperty.Register(
            nameof(HasPassed),
            typeof(bool),
            typeof(InputValidationHost),
            new(false));

        public bool HasError
        {
            get => (bool)GetValue(HasErrorProperty);
            set => SetValue(HasErrorProperty, value);
        }

        public static readonly DependencyProperty HasErrorProperty = DependencyProperty.Register(
            nameof(HasError),
            typeof(bool),
            typeof(InputValidationHost),
            new(false));

        public bool IsUnvalidated
        {
            get => (bool)GetValue(IsUnvalidatedProperty);
            set => SetValue(IsUnvalidatedProperty, value);
        }

        public static readonly DependencyProperty IsUnvalidatedProperty = DependencyProperty.Register(
            nameof(IsUnvalidated),
            typeof(bool),
            typeof(InputValidationHost),
            new(true));

        public event EventHandler<ValidationEventArgs> Validate;
        public event EventHandler<ValidationEventArgs> Validated;
        public event EventHandler<ValidationEventArgs> Passed;
        public event EventHandler<ValidationEventArgs> Error;
        public event EventHandler<ValidationEventArgs> StateChanged;

        public Func<Control, object> GetContent { get; set; }

        private void OnValidate()
        {
            Validate?.Invoke(this, new() { State = State });
        }

        private void OnValidated()
        {
            Validated?.Invoke(this, new() { State = State });
        }

        private void OnPassed()
        {
            Passed?.Invoke(this, new() { State = State });
        }

        private void OnError()
        {
            Error?.Invoke(this, new() { State = State });
        }

        private static void StateChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs args)
        {
            var control = (InputValidationHost)d;
            control.SetValue(HasPassedProperty, control.State == InputValidationState.Passed);
            control.SetValue(HasErrorProperty, control.State == InputValidationState.Error);
            control.SetValue(IsUnvalidatedProperty, control.State == InputValidationState.Unvalidated);
            control.StateChanged?.Invoke(control, new() { State = control.State });
        }

        public void TriggerValidation(object content)
        {
            OnValidate();
            var isPassed = Validators.All(validator => validator(content));
            if (isPassed)
            {
                State = InputValidationState.Passed;
                OnPassed();
            }
            else
            {
                State = InputValidationState.Error;
                OnError();
            }

            OnValidated();
        }

        public void TriggerValidation()
        {
            TriggerValidation(GetContent(InputControl));
        }
    }
}

我的用法:

<controls:InputValidationHost x:Name="InputValidation" ErrorMessage="Error" PassedMessage="Passed" UnvalidatedMessage="Unvalidated">
    <TextBox KeyUp="OnEnterPressed" />
</controls:InputValidationHost>

这应该会对依赖属性HasErrorHasPassedIsUnvalidated做出React,并将更改应用到InputControlCaptionText。但是当我使用此控件时,其中包含一个TextBox,得到的只是一个没有CaptionText的默认TextBox。我确信我已经设置了***Message,并且状态更改正确。
你可以找到一个reproduction on GitHub

ybzsozfc

ybzsozfc1#

要更改VisualStates,需要调用VisualStateManager.GoToState()方法:

VisualStateManager.GoToState(this, "Error", useTransitions: false);

更新

我看了一下你的repo,恕我直言,你应该使用GoToState()而不是StateTriggersIsActive会在True时更改VisualState,但在False时不会更改VisualState。所以,我想你最好显式更改VisualStates
这是一个基本的例子:

通用.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:VisualStateTests">

    <Style TargetType="local:CustomTextBox">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CustomTextBox">
                    <Grid>
                        <TextBox x:Name="TextBoxControl" />
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="Red">
                                    <VisualState.Setters>
                                        <Setter Target="TextBoxControl.Foreground" Value="Red" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Green">
                                    <VisualState.Setters>
                                        <Setter Target="TextBoxControl.Foreground" Value="Green" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Blue">
                                    <VisualState.Setters>
                                        <Setter Target="TextBoxControl.Foreground" Value="Blue" />
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

自定义文本框.cs

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace VisualStateTests;

public sealed class CustomTextBox : Control
{
    public CustomTextBox()
    {
        this.DefaultStyleKey = typeof(CustomTextBox);
    }

    private TextBox? TextBoxControl { get; set; }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (GetTemplateChild(nameof(TextBoxControl)) is TextBox textBoxControl)
        {
            TextBoxControl = textBoxControl;
            TextBoxControl.TextChanged += TextBoxControl_TextChanged; ;
        }
    }

    private void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e)
    {
        UpdateVisualState();
    }

    private void UpdateVisualState()
    {
        bool useTransitions = false;
        bool result = TextBoxControl?.Text.ToLower() switch
        {
            "red" => VisualStateManager.GoToState(this, "Red", useTransitions),
            "green" => VisualStateManager.GoToState(this, "Green", useTransitions),
            "blue" => VisualStateManager.GoToState(this, "Blue", useTransitions),
            _ => VisualStateManager.GoToState(this, "Normal", useTransitions),
        };
    }
}

如果您无论如何都需要使用StateTriggers,您可以找到一个很好的示例代码here

相关问题