将ViewModel的事件绑定到XAML

时间:2017-07-11 01:40:06

标签: c# wpf xaml mvvm

在我的虚拟机中,我有一个事件

public class ViewModelBase
{
  public delegate bool MyDel(object param);
  public MyDel MyEvent;

 public void TransferClick()
 {
      MyEvent(null); // to simulate the click at View
 }
}

在视图中,目前我有以下代码(后面):

public class View: UserControl
{
   private void UserControl1_Load(Object sender, EventArgs e) 
 {

  (DataContext as ViewModelBase).MyEvent+=SimulateClick;

  }
  private bool SimulateClick(object param)  
{ 
   //some logic to simulate clicks on the View, on the user control
}
}

这样,只要有必要,VM就可以在View中调用SimulateClick逻辑。

我不喜欢这种方法,因为它污染了我的观点背后的代码。任何使MyEvent绑定到XAML的方法,就像我将VM ICommand绑定到现有按钮点击和类似的东西一样?

注意:我实际上 想要模拟鼠标点击(我知道我可以使用ICommand来做到这一点),只是想要在我的MVVM模型上做一些事件,比如鼠标点击事件。

5 个答案:

答案 0 :(得分:2)

如果您查看模型需要告诉视图执行某些操作,您可以使用事件聚合器或信使以松散耦合的方式将视图模型中的消息发送到视图:

https://blog.magnusmontin.net/2014/02/28/using-the-event-aggregator-pattern-to-communicate-between-view-models/

https://msdn.microsoft.com/en-us/magazine/jj694937.aspx

使用此模式的好处是视图模型和视图不需要彼此了解任何内容。

另一种选择是使用视图实现的接口类型注入视图模型:

public interface IView
{
    bool SimulateClick(object param);
}

public partial class View : UserControl, IView
{
    public View()
    {
        InitializeComponent();
        DataContext = new ViewModel(this);
    }

    public bool SimulateClick(object param)
    {
        //...
    }

}

这并没有真正打破MVVM模式,因为视图模型只依赖于视图恰好实现的接口。

答案 1 :(得分:1)

  

VM将从其他VM接收命令(想想FileVM)   通知tabVM),然后VM(tabVM)将加载数据   通过View(tabView)上的SimulateClick方法查看。

如果方法根据某些命令加载数据,为什么要将方法命名为SimulateClick?我会像这样重构你的代码。

++x

如果您公开了这样的功能,可以在代码隐藏中附加到事件。为什么要附加到VM的事件并调用代码隐藏方法来污染XAML?最好在代码隐藏中附加事件,因为至少你的代码保持类型安全和可重构。

public delegate bool MyDel(object data);

public class ViewModelBase
{
   public MyDel SomeCommandExecuted;

   void SomeCommand_Execute()
   {
      string[] sampleData = new [] //this may come from the other viewmodel for example
      {
         "Item1", "Item2", "Items3";
      }

      MyDel handler = SomeCommandExecuted;
      if (handler != null)
      {
          handler(sampleData);
      }
   }
}

在代码隐藏中附加到VM的事件不违反MVVM。但是,ViewModel的可靠性是以可以从View轻松获取的形式公开数据(通常通过数据绑定)。

这是我的建议:

public class View: UserControl
{
   public View()
   {
       this.InitializeComponent();
       Loaded += (o, e) => ViewModel.SomeCommandExecuted += ViewModel_SomeCommandExecuted;
   }

   ViewModelBase ViewModel => (ViewModelBase)DataContext;


  private bool ViewModel_SomeCommandExecuted(object data)  
  { 
     //load the data into view
  }
}

我已在ViewModel中准备好数据,以便在视图中轻松使用它们。准备好后,我会使用public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected bool InDesignMode => DesignerProperties.GetIsInDesignMode(new DependecyObject()); } public class ViewModel : ViewModelBase { private string[] _data; //ctor public ViewModel() { if (IsInDesignMode) { Data = new [] { "Visible", "In", "XAML", "Designer" } } } public string[] Data { get { return _data; } set { _data = value; OnPropertyChanged(); } } void SomeCommand_Execute() { string[] sampleData = new [] //this may come from the other viewmodel for example { "Item1", "Item2", "Items3"; } Data = sampleData; } } 事件通知视图。现在我可以在View中轻松绑定ItemsControl,ListView等。没有代码隐藏需要什么。这是ViewModel的目的

答案 2 :(得分:1)

更新回答

首先 - 我强烈推荐@ mm8建议的方法,或者在你的视图上公开Command(s)(如RefreshCommand)来实现同样的目标。

但如果那不是一个选择;那么我相信你可以创建一个自定义附加事件,它可以在技术上将视图模型的事件绑定到控件的事件处理程序;同时保持MVVM的分离水平。

例如,您可以按以下方式定义附加事件:

// ViewModel event args 
public class MyEventArgs : EventArgs
{
    public object Param { get; set; }
}

// Interim args to hold params during event transfer    
public class InvokeEventArgs : RoutedEventArgs
{
    public InvokeEventArgs(RoutedEvent e) : base(e) { }

    public object Param { get; set; }
}    

// Base view model
public class ViewModelBase
{
    public event EventHandler<MyEventArgs> MyEvent1;
    public event EventHandler<MyEventArgs> MyEvent2;

    public void TransferClick1()
    {
        MyEvent1?.Invoke(this, new MyEventArgs { Param = DateTime.Now }); // to simulate the click at View
    }

    public void TransferClick2()
    {
        MyEvent2?.Invoke(this, new MyEventArgs { Param = DateTime.Today.DayOfWeek }); // to simulate the click at View
    }
}

// the attached behavior that does the magic binding
public class EventMapper : DependencyObject
{
    public static string GetTrackEventName(DependencyObject obj)
    {
        return (string)obj.GetValue(TrackEventNameProperty);
    }

    public static void SetTrackEventName(DependencyObject obj, string value)
    {
        obj.SetValue(TrackEventNameProperty, value);
    }

    public static readonly DependencyProperty TrackEventNameProperty =
        DependencyProperty.RegisterAttached("TrackEventName",
            typeof(string), typeof(EventMapper), new PropertyMetadata
            (null, new PropertyChangedCallback(OnTrackEventNameChanged)));

    private static void OnTrackEventNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie == null)
            return;

        var eventName = GetTrackEventName(uie);
        if (string.IsNullOrWhiteSpace(eventName))
            return;

        EventHandler<MyEventArgs> vmEventTracker = delegate (object sender, MyEventArgs e) {
            Application.Current.Dispatcher.Invoke(() =>
                uie.RaiseEvent(new InvokeEventArgs(EventMapper.OnInvokeEvent)
                {
                    Source = sender,
                    Param = e?.Param
                }));
        };

        uie.DataContextChanged += (object sender, DependencyPropertyChangedEventArgs e) =>
        {
            var oldVM = e.OldValue;
            var newVM = e.NewValue;

            if (oldVM != null)
            {
                var eventInfo = oldVM.GetType().GetEvent(eventName);
                eventInfo?.RemoveEventHandler(oldVM, vmEventTracker);
            }

            if (newVM != null)
            {
                var eventInfo = newVM.GetType().GetEvent(eventName);
                eventInfo?.AddEventHandler(newVM, vmEventTracker);
            }
        };

        var viewModel = uie.DataContext;
        if (viewModel != null)
        {
            var eventInfo = viewModel.GetType().GetEvent(eventName);
            eventInfo?.AddEventHandler(viewModel, vmEventTracker);
        }
    }

    public static readonly RoutedEvent OnInvokeEvent =
        EventManager.RegisterRoutedEvent("OnInvoke",
            RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(EventMapper));
    public static void AddOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie != null)
        {
            uie.AddHandler(OnInvokeEvent, handler);
        }
    }

    public static void RemoveOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie != null)
        {
            uie.RemoveHandler(OnInvokeEvent, handler);
        }
    }
}

示例1 - 事件处理程序

XAML用法

<StackPanel Margin="20">
    <Button Margin="10" Content="Invoke VM event" Click="InvokeEventOnVM" />        
    <Button Content="View Listener1" 
            local:EventMapper.TrackEventName="MyEvent1"
            local:EventMapper.OnInvoke="SimulateClick1" />

    <Button Content="View Listener2" 
            local:EventMapper.TrackEventName="MyEvent1"
            local:EventMapper.OnInvoke="SimulateClick1" />

    <Button Content="View Listener3" 
            local:EventMapper.TrackEventName="MyEvent2"
            local:EventMapper.OnInvoke="SimulateClick2" />

</StackPanel>

以上XAML的示例代码 - 后面:

private void SimulateClick1(object sender, RoutedEventArgs e)
{
    (sender as Button).Content = new TextBlock { Text = (e as InvokeEventArgs)?.Param?.ToString() };
}

private void SimulateClick2(object sender, RoutedEventArgs e)
{
    SimulateClick1(sender, e);
    (sender as Button).IsEnabled = !(sender as Button).IsEnabled; //toggle button
}

private void InvokeEventOnVM(object sender, RoutedEventArgs e)
{
    var vm = new ViewModelBase();
    this.DataContext = vm;

    vm.TransferClick1();
    vm.TransferClick2();
}

enter image description here

样本2 - 事件触发器(更新07/26)

XAML用法

<Button Content="View Listener" 
    local:EventMapper.TrackEventName="MyEvent2">
    <Button.Triggers>
        <EventTrigger RoutedEvent="local:EventMapper.OnInvoke">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:1" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Button.Triggers>
</Button>

答案 3 :(得分:0)

您可以使用x:bind进行功能绑定。这样你的xaml可以绑定并直接调用视图模型事件处理程序,而不需要&#34;传递&#34;在视图中调用方法。

Click="{x:Bind viewModel.Foo}"

More docs

以下是wpf

中事件绑定的示例

WPF event binding from View to ViewModel?

答案 4 :(得分:0)

  

的xmlns:I =&#34; HTTP://schemas.microsoft.com/expression/2010/interactivity"

 <StackPanel Background="Transparent">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="Tap">
      <command:EventToCommand
        Command="{Binding Main.NavigateToArticleCommand,
          Mode=OneWay,
          Source={StaticResource Locator}}"
        CommandParameter="{Binding Mode=OneWay}" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</StackPanel>

<强>视图模型

public RelayCommand NavigateToArticleCommand 
{ 
 get 
{ 

  return _navigateToArticleCommand
      ?? (_navigateToArticleCommand= new RelayCommand( 
        async () => 
        { 
          await SomeCommand(); 
        })); 
  } 
}