Событие Bind ViewModel для XAML


1 принят

Обновленный ответ

Прежде всего, я бы очень рекомендовал подход @ mm8 предложил или обнародовал команды (такие как 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();
}

введите описание изображения здесь

Пример 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>

1

Если вы хотите, чтобы модель отображала представление, чтобы вы могли что-то сделать, вы могли бы использовать агрегатор событий или мессенджер для отправки сообщения из модели представления в представление по слабому соединению:

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, поскольку модель представления имеет только зависимость от интерфейса, который реализуется в представлении.


0

Вы можете использовать x: bind для привязки функции. Таким образом, ваш xaml может связывать и напрямую вызывать обработчик событий моделей представлений, не требуя метода «пройти через» в представлении.

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

Дополнительные документы

Вот пример привязки события в wpf

Событие WPF привязки от View to ViewModel?


0

XMLNS: я = "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>

ViewModel

public RelayCommand NavigateToArticleCommand 
{ 
 get 
{ 

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

0

VM получит команду от других виртуальных машин (подумайте, что FileVM уведомляет tabVM), а затем VM (tabVM) будет загружать данные в представление через метод SimulateClick в представлении (tabView).

Почему, черт возьми, вы бы назвали метод SimulateClick, если метод загружает данные на основе некоторой команды? Я бы реорганизовал ваш код так.

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);
      }
   }
}

если вы раскрываете такую ??функциональность, нормально присоединяться к событию в коде. Почему вы загрязняете XAML, присоединяясь к событию виртуальной машины и вызывая метод codebehind? Лучше присоединяться к событию в коде, потому что по крайней мере ваш код остается безопасным и рефакторизуемым.

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
  }
}

Присоединение к событию виртуальной машины в кодебе не является нарушением MVVM. Тем не менее, resposibility 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;
   }
}

Я подготовил данные в ViewModel, чтобы они были легко расходуемыми. Как только они будут готовы, я уведомлю о просмотре с помощью PropertyChangedсобытия. Теперь я могу легко связать ItemsControl, ListView и т. Д. В представлении. Никакая кодовая потребность не требовала того, что когда-либо было. Это цель ViewModel

C #, WPF, XAML, MVVM,

c#,wpf,xaml,mvvm,

0

Ответов: 5


1 принят

Обновленный ответ

Прежде всего, я бы очень рекомендовал подход @ mm8 предложил или обнародовал команды (такие как 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();
}

введите описание изображения здесь

Пример 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>

1

Если вы хотите, чтобы модель отображала представление, чтобы вы могли что-то сделать, вы могли бы использовать агрегатор событий или мессенджер для отправки сообщения из модели представления в представление по слабому соединению:

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, поскольку модель представления имеет только зависимость от интерфейса, который реализуется в представлении.


0

Вы можете использовать x: bind для привязки функции. Таким образом, ваш xaml может связывать и напрямую вызывать обработчик событий моделей представлений, не требуя метода «пройти через» в представлении.

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

Дополнительные документы

Вот пример привязки события в wpf

Событие WPF привязки от View to ViewModel?


0

XMLNS: я = "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>

ViewModel

public RelayCommand NavigateToArticleCommand 
{ 
 get 
{ 

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

0

VM получит команду от других виртуальных машин (подумайте, что FileVM уведомляет tabVM), а затем VM (tabVM) будет загружать данные в представление через метод SimulateClick в представлении (tabView).

Почему, черт возьми, вы бы назвали метод SimulateClick, если метод загружает данные на основе некоторой команды? Я бы реорганизовал ваш код так.

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);
      }
   }
}

если вы раскрываете такую ??функциональность, нормально присоединяться к событию в коде. Почему вы загрязняете XAML, присоединяясь к событию виртуальной машины и вызывая метод codebehind? Лучше присоединяться к событию в коде, потому что по крайней мере ваш код остается безопасным и рефакторизуемым.

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
  }
}

Присоединение к событию виртуальной машины в кодебе не является нарушением MVVM. Тем не менее, resposibility 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;
   }
}

Я подготовил данные в ViewModel, чтобы они были легко расходуемыми. Как только они будут готовы, я уведомлю о просмотре с помощью PropertyChangedсобытия. Теперь я могу легко связать ItemsControl, ListView и т. Д. В представлении. Никакая кодовая потребность не требовала того, что когда-либо было. Это цель ViewModel

C #, WPF, XAML, MVVM,
Похожие вопросы