在WPF MVVM中使用Toast通知

时间:2017-01-08 19:55:21

标签: c# wpf custom-controls

我在WPF中创建了一个自定义控件'Toast Notification',我想在MVVM中使用它来显示消息,但我不知道它在哪里实现它(MainWindow或每个页面)以及如何从每个页面发送消息to toast通知。请帮我解决这个问题。

my toast notification control

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

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate ();
        ChangeVisualState ();
    }

    public static readonly DependencyProperty TextProperty=
        DependencyProperty.Register ("Text",typeof (string),typeof (Toast),new PropertyMetadata ("Sample Text"));

    public static readonly DependencyProperty ToastIconProperty=
        DependencyProperty.Register ("ToastIcon",typeof (ToastIconType),typeof (Toast),new PropertyMetadata (ToastIconType.None,OnToastIconChanged));

    public static readonly DependencyProperty IsToastVisibleProperty=
        DependencyProperty.Register ("IsToastVisible",typeof (bool),typeof (Toast),new PropertyMetadata (false,OnIsToastVisibleChanged));

    public static readonly DependencyProperty DurationProperty=
        DependencyProperty.Register ("Duration",typeof (TimeSpan),typeof (Toast),new PropertyMetadata (TimeSpan.FromSeconds (10),OnDurationChanged));

    public static readonly DependencyProperty ImageGeometryProperty=
        DependencyProperty.Register ("ImageGeometry",typeof (Geometry),typeof (Toast));

    public string Text
    {
        get { return (string)GetValue (TextProperty); }
        set { SetValue (TextProperty,value); }
    }

    public bool IsToastVisible
    {
        get { return (bool)GetValue (IsToastVisibleProperty); }
        set { SetValue (IsToastVisibleProperty,value); ChangeVisualState (); }
    }

    public ToastIconType ToastIcon
    {
        get { return (ToastIconType)GetValue (ToastIconProperty); }
        set { SetValue (ToastIconProperty,value); }
    }

    public TimeSpan Duration
    {
        get { return (TimeSpan)GetValue (DurationProperty); }
        set { SetValue (DurationProperty,value); }
    }

    public Geometry ImageGeometry
    {
        get { return (Geometry)GetValue (ImageGeometryProperty); }
        set { SetValue (ImageGeometryProperty,value); }
    }

    private static void OnDurationChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
    {
        var control=(Toast)d;
        var value=(TimeSpan)e.NewValue;
        control.Duration=value;
    }

    private static void OnIsToastVisibleChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
    {
        var c=(Toast)d;
        var value=(bool)e.NewValue;
        c.IsToastVisible=value;
    }

    private void ChangeVisualState()
    {
        if (IsToastVisible)
        {
            DoubleAnimation da=new DoubleAnimation { From=1,To=0,Duration=TimeSpan.FromSeconds (Duration.Seconds) };

            CubicEase cubicEase=new CubicEase ();
            cubicEase.EasingMode=EasingMode.EaseInOut;

            da.EasingFunction=cubicEase;

            da.Completed+=(sender,e) => IsToastVisible=false;
            BeginAnimation (OpacityProperty,da);
        }
        else
        {
            Opacity=0;
        }
    }

    private static void OnToastIconChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
    {
        var control=(Toast)d;
        var value=(ToastIconType)e.NewValue;

        switch (value)
        {
            case ToastIconType.Information:
                control.ImageGeometry=Geometry.Parse ("M3.6069946,1.8659973C2.6459963,1.8659973,1.8660278,2.6480103,1.8660278,3.6080017L1.8660278,20.546021C1.8660278,21.507019,2.6459963,22.288025,3.6069946,22.288025L11.647035,22.288025 9.6170056,27.481018 18.038026,22.288025 28.124027,22.288025C29.085025,22.288025,29.865054,21.507019,29.865054,20.546021L29.865054,3.6080017C29.865054,2.6480103,29.085025,1.8659973,28.124027,1.8659973z M3.6069946,0L28.124027,0C30.11304,0,31.731998,1.6190186,31.731998,3.6080017L31.731998,20.546021C31.731998,22.536011,30.11304,24.154022,28.124027,24.154022L18.567018,24.154022 5.8439948,32 8.9130261,24.154022 3.6069946,24.154022C1.618042,24.154022,-2.120687E-07,22.536011,0,20.546021L0,3.6080017C-2.120687E-07,1.6190186,1.618042,0,3.6069946,0z");
                break;

            case ToastIconType.Warning:
                control.ImageGeometry=Geometry.Parse ("M13.950004,24.5L13.950004,28.299988 17.950004,28.299988 17.950004,24.5z M13.950004,10.399963L13.950004,21.699951 17.950004,21.699951 17.950004,10.399963z M15.950004,0C16.349998,0,16.750007,0.19995117,16.950004,0.69995117L31.750011,30.099976C31.949993,30.5 31.949993,31 31.750011,31.399963 31.549999,31.799988 31.150005,32 30.750011,32L1.1499981,32C0.75000406,32 0.34999478,31.799988 0.14999761,31.399963 -0.049999204,31 -0.049999204,30.5 0.14999761,30.099976L14.950004,0.69995117C15.150001,0.19995117,15.549995,0,15.950004,0z");
                break;
        }
    }

    public enum ToastIconType
    {
        None=0,
        Information=1,
        Warning=2
    }
}

和我的吐司风格:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MetroCustomControls">
    <Style TargetType="{x:Type local:Toast}">
        <Style.Resources>
            <SolidColorBrush x:Key="Toast.Static.Background" Color="LightGray" />
            <SolidColorBrush x:Key="Toast.Static.BorderBrush" Color="Gray" />
            <SolidColorBrush x:Key="Toast.Static.Foreground" Color="DimGray" />
        </Style.Resources>
        <Setter Property="Background" Value="{StaticResource Toast.Static.Background}" />
        <Setter Property="BorderBrush" Value="{StaticResource Toast.Static.BorderBrush}" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Foreground" Value="{StaticResource Toast.Static.Foreground}" />
        <Setter Property="FontFamily" Value="Segoe UI Light" />
        <Setter Property="FontSize" Value="14" />
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="HorizontalContentAlignment" Value="Center" />
        <Setter Property="VerticalAlignment" Value="Bottom" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="Margin" Value="0,0,0,10" />
        <Setter Property="Focusable" Value="False" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:Toast}">
                    <Grid x:Name="template_Root">
                        <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"
                                CornerRadius="2"  HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}" Margin="0,0,0,20">
                            <StackPanel Orientation="Horizontal">
                                <Path x:Name="path" Margin="6,0,0,0" Fill="{TemplateBinding Foreground}"  Stretch="Uniform" Width="12" Height="12" Data="{TemplateBinding ImageGeometry}" />
                                <TextBlock  Text="{TemplateBinding Text}" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" Padding="5" />
                            </StackPanel>
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <DataTrigger Binding="{Binding ToastIcon,RelativeSource={RelativeSource Self}}" Value="None">
                            <Setter TargetName="path" Property="Visibility" Value="Collapsed" />
                        </DataTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

1 个答案:

答案 0 :(得分:3)

这类似于MVVM中的Dialogs问题。

理想的解决方案是将功能包装在DialogService或您的ToastService中,您可以将其注入需要它的ViewModel中。

因此您的MainWindow或根ViewModel具有控件,并且能够显示Toasts。然后你有IToastService看起来像这样:

public class ToastService
{
    public event Action<String> ToastMessageRecieved;

    public void ShowToast(string message)
    {
        ToastMessageRecieved(message);
    }
}

ShowToast操作的消费者(实际上显示吐司的内容),比如RootViewModel,可以订阅ToastMessageRecieved操作:

public MyRootViewModel(ToastService toastService)
{
    //keep it as a dependency in case we want to show toasts
    this.toastService = toastService;

    toastService.ToastMessageRecieved += (message) =>
    {
        //here's where you actually show your toast, however that's done
        //MyRootViewModel has the actual UI element reference. It only
        //appears in this one place.
        Toast.Message = message;
    };
}

任何其他想要显示Toast的ViewModel都会使用该服务:

public MyRandomToastGeneratingViewModel(ToastService toastService)
{
    //our service is inejcted as a dependency
    this.toastService = toastService;
}

public void ShowAToastButtonPressed()
{
    toastService.ShowToast("My great toast!");
}

理想情况下,您可以使用某种依赖注入框架 - 但它值得为此类事情实施。对话服务,例如你的祝酒词,工厂和数据访问都适合MVVM和DI。

要将实际控件(您拥有的单个实例)放在其他所有内容之上,在Window或RootView中,您可以让它占用与普通内容相同的网格单元格。这就是我通常做重叠控件的方法:

<Window x:Class="MyApp.MainWindow"
        ...>
    <Grid>
        //my normal content
        <myControls:Toast VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="20"/>
    </Grid>
</Window>

按照添加顺序,它们将占据相同的空间。