Событие в C# WPF MVVM

147
12 марта 2019, 16:00

Есть 2 ViewModel`и AuthViewModel и LoginViewModel. В LoginView есть Frame который контент которого LoginControl а у него контекст AuthViewModel. И в этом LoginControl есть кнопка при нажатии на которую должно срабатывать события LoginCompleted и в App.xaml.cs идет обработка события.

AuthViewModel.cs

 public event Action LoginCompleted;
    public ICommand AuthCommand => new RelayCommand(o => AuthMethod());

    public void AuthMethod()
    {
        LoginCompleted?.Invoke();
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

App.xaml.cs

private LoginWindow Window { get; set; }
    private MainWindow CustomWindow { get; set; }
    public MainLoginVIewModel MainViewModel { get; set; }
    public LoginViewModel LoginViewModel { get; set; }
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        LoginViewModel = new LoginViewModel();
        MainViewModel = new MainLoginVIewModel(LoginViewModel);
        LoginViewModel.OnAuthorize += LoginViewModelOnOnAuthorize;
        Window = new LoginWindow { DataContext = MainViewModel };
        Window.Show();
    }
    private void LoginViewModelOnOnAuthorize(object sender, LoginEventArgs e)
    {
        if (e.IsAuthorized)
        {
            CustomWindow = new MainWindow { DataContext = new ContentViewModel(e.User) };
            CustomWindow.Show();
            Window.Close();
        }
    }

Тоесть нажимая на кнопку в LoginControl должно срабатывать событие в App.xaml.cs Если я что то не то делаю подскажите как реализовать правильней.

Если нужно еще скину кода

RegisterControl.xaml

<Grid>
    <TextBox Text="{Binding User}" Width="100" Height="100"></TextBox>
</Grid>

RegisterViewModel.cs

class RegisterViewModel : VM
{
    private string user;
    public string User
    {
        get => user;
        set => Set(ref user, value);
    }
}

ViewModel главного окна авторизации у меня оно называется MainLoginViewModel

RegisterViewModel regVM;
    public MainLoginVIewModel(LoginViewModel loginVM)
    {
        TestCommand = new RelayCommand(Test);
        regVM = new RegisterViewModel();
        CurrentContent = loginVM;
    }
    private VM currentContent;
    public VM CurrentContent
    {
        get => currentContent;
        set => Set(ref currentContent, value);
    }
    public ICommand TestCommand { get; }
    private void Test()
    {
        CurrentContent = regVM;
    }
Answer 1

Если так бегло посмотреть на ваше описание, то у вас тут как минимум 3 проблемы:

  1. Вы довольно много логики делаете в App, это не очень удачное решение. Мне кажется, что там это даже не логично размещать. Сделайте для таких целей отдельную VM, которая все обработает, зачем же в APP...
  2. В MVVM нельзя работать с контролами через код, если у вас есть скажем TextBox с именем x:Name="textBox1" и вы хотите задать ему текст таким путем: textBox1.Text = "...", то тут вы напрямую работаете с этим элементом, что по правилам MVVM очень плохо! Для таких целей существую привязки (вы указываете <TextBox Text="{Binding MyTextProperty}">). Попробуйте убрать все имена у контролов, а то и вовсе убрать View часть. По паттерну MVVM ваше приложение должно работать даже без View.
  3. Frame, это не очень хороший контрол для отображения содержимого в окне, он плохо встраивается в MVVM паттерн. Попробуйте использовать этот вариант и станет в разы легче!

Давайте попробуем сделать простой пример с реализацией всего, что я написал выше. Сделаем некий аналог авторизации с событием и сменой контента:

Я начну это все на пустом проекте, вы же смотрите сами...

И так, для начала нам понадобится два вспомогательных класса:

  • VM - Он реализует INotifyPropertyChanged, а также мы по нему будем задавать текущий контент. У всех реализация разная, я лично возьму такой вариант:

    public class VM : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;
            field = value;
            NotifyPropertyChanged(propertyName);
            return true;
        }
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
  • RelayCommand В MVVM не принято использовать события контролов (Click и подобные), их заменяют на привязки и что бы нам привязать логику на кнопку используют как раз команды (ICommand). RelayCommand соответственно является реализацией данного интерфейса. И опять же, у всех она разная, я лично возьму этот вариант:

    public class RelayCommand<T> : ICommand
    {
        private Action<T> action;
        public RelayCommand(Action<T> action) => this.action = action;
        public bool CanExecute(object parameter) => true;
        #pragma warning disable CS0067
        public event EventHandler CanExecuteChanged;
        #pragma warning restore CS0067
        public void Execute(object parameter) => action((T)parameter);
    }
    public class RelayCommand : ICommand
    {
        private Action action;
        public RelayCommand(Action action) => this.action = action;
        public bool CanExecute(object parameter) => true;
        #pragma warning disable CS0067
        public event EventHandler CanExecuteChanged;
        #pragma warning restore CS0067
        public void Execute(object parameter) => action();
    }
    

Отлично, подготовка завершена. Теперь сделаем две страницы и VM под них.

  1. Страница авторизации. На ней мы попросим логин и пароль пользователя, а в ее VM мы реализуем команду, которая будет вызвана при попытки авторизации. Сама LoginViewModel будет наследоваться от класса VM (так мы сможем ее потом применить при смене контента, а также использовать INPC).

    • LoginViewModel:

      public class LoginViewModel : VM
      {
          public event EventHandler<LoginEventArgs> OnAuthorize;
          public ICommand LoginCommand { get; }
          public LoginViewModel()
          {
              LoginCommand = new RelayCommand(Authorize);
          }
          private string user;
          public string User
          {
              get => user;
              set => Set(ref user, value);
          }
          private string password;
          public string Password
          {
              get => password;
              set => Set(ref password, value);
          }
          private void Authorize()
          {
              if (User?.ToLower() == "test" && Password?.ToLower() == "123")
              {
                  OnAuthorize?.Invoke(this, new LoginEventArgs(User, true, "Успешная авторизация!"));
              }
              else
              {
                  OnAuthorize?.Invoke(this, new LoginEventArgs(User, false, "Неверный логин или пароль!"));
              }
          }
      }
      
    • LoginEventArgs - это данные события (EventArgs), простой класс, который содержит структуру передаваемых данных в этом событии:

      public class LoginEventArgs : EventArgs
      {
          public string User { get; }
          public bool IsAuthorized { get; }
          public string Message { get; }
          public LoginEventArgs(string user, bool isAuthorized, string message)
          {
              User = user;
              IsAuthorized = isAuthorized;
              Message = message;
          }
      }
      
    • LoginView - Это View нашей авторизации, всего два поля и кнопка, все привязываем к свойствам из LoginViewModel. Сама View является пользовательским элементом управления (UserControl):

      <Grid VerticalAlignment="Center" HorizontalAlignment="Center">
          <Grid.RowDefinitions>
              <RowDefinition/>
              <RowDefinition/>
              <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto"/>
              <ColumnDefinition/>
          </Grid.ColumnDefinitions>
          <TextBlock Grid.Column="0" Grid.Row="0" Text="Логин: "/>
          <TextBlock Grid.Column="0" Grid.Row="1" Text="Пароль: "/>
          <TextBox Grid.Column="1" BorderThickness="0 0 0 1" Grid.Row="0" Text="{Binding User}" Width="100"/>
          <TextBox Grid.Column="1" BorderThickness="0 0 0 1"  Grid.Row="1" Text="{Binding Password}" Width="100"/>
          <Button Grid.Row="2" Margin="0 5" Background="Transparent" Grid.ColumnSpan="2" Grid.Column="0" Content="Войти" Command="{Binding LoginCommand}"/>
      </Grid>
      
  2. Страница с контентом, который мы покажем после авторизации. Делаем все в точности по аналогии с LoginView:

    • ContentViewModel - Самая простейшая ViewModel в которой я для примера буду содержать всего одно свойство. ViewModel должна наследоваться от класса VM, иначе не сможем задать как контент:

      public class ContentViewModel : VM
      {
          public string Title { get; set; } = "У нас получилось!";
      }
      
    • ContentView - View нашего основного контента. Является UserControl, содержит всего лишь привязанный TextBlock:

      <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Text="{Binding Title}"/>
      

Теперь давайте поработаем над основным окном нашего приложения (MainWindow). Для него мы сделаем тоже свою ViewModel, назовем MainViewModel и в ней мы все это объединим:

public class MainViewModel : VM
{
    public LoginViewModel LoginViewModel { get; }
    public MainViewModel()
    {
        LoginViewModel = new LoginViewModel();
        LoginViewModel.OnAuthorize += LoginViewModelOnOnAuthorize;
        CurrentContent = LoginViewModel;
    }
    private VM currentContent;
    public VM CurrentContent
    {
        get => currentContent;
        set => Set(ref currentContent, value);
    }
    private string message;
    public string Message
    {
        get => message;
        set => Set(ref message, value);
    }
    private void LoginViewModelOnOnAuthorize(object sender, LoginEventArgs e)
    {
        Message = $"[{e.User}] {e.Message}";
        CurrentContent = e.IsAuthorized ? (VM) new ContentViewModel() : LoginViewModel;
    }
}

Можно заметить, что мы создаем свойство ViewModel авторизации, в конструкторе инициализируем, подписываемся на событие и для примера задаем как основной контент нашего окна. В обработчики события я для примера формирую сообщение, которое в присваиваю созданному свойству.

Теперь само MainWindow:

<Grid>
    <Grid.Resources>
        <DataTemplate DataType="{x:Type vm:ContentViewModel}">
            <view:ContentView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:LoginViewModel}">
            <view:LoginView/>
        </DataTemplate>
    </Grid.Resources>
    <StackPanel>
        <Border BorderThickness="0 0 0 1" BorderBrush="Gray" Padding="10" Margin="10">
            <TextBlock Text="{Binding Message}"/>
        </Border>
        <ContentPresenter Content="{Binding CurrentContent}"/>
    </StackPanel>
</Grid>

С помощью DataTemplate связываем каждую ViewModel со своей View. Также выводим сообщение (обычный TextBlock, который привязан к свойству Message), ну и контент страницы (им мы заменяем Frame), задаем мы его с помощью ContentPresenter.

Осталось нам задать DataContext, сделаем мы это переписав немного App:

  • В App.xaml убираем StartupUri="MainWindow.xaml".
  • В App.xaml.cs переопределяем OnStartup:

    private MainWindow Window { get; set; }
    public MainViewModel MainViewModel { get; set; }
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        MainViewModel = new MainViewModel();
        Window = new MainWindow{DataContext = MainViewModel};
        Window.Show();
    }
    

Все, запускаем и любуемся проделанной работой:

Вот так, довольно просто, мы избавились от ужасного Frame, использовали событие и вывели разное содержимое в одно окно. Заметьте, мы не разу не обратились к элементу какой либо View, мы можем их вообще убрать и наша логика будет спокойно жить свой жизнью. Вот это разбитие по слоям и есть MVVM! Вы главное поймите принцип, как все это работает и дальше пойдет все как по маслу! Удачи!

Если у нас разные окна, то самый простой вариант, это сделать следующее:

  1. Перепишем MainVewModel, а точней его конструктор, пусть он принимает LoginViewModel:

    public MainViewModel(LoginViewModel loginVM)
    {
        LoginViewModel = loginVM;
        CurrentContent = LoginViewModel;
    }
    
  2. Перепишем App. Тут нам надо инициализировать LoginVM, MainVM, подписаться на события, в событие сделать логику открытия/закрытия и задать свойства окон:

    public partial class App : Application
    {
        private MainWindow Window { get; set; }
        private CustomWindow CustomWindow { get; set; }
        public MainViewModel MainViewModel { get; set; }
        public LoginViewModel LoginViewModel { get; set; }
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            LoginViewModel = new LoginViewModel();
            MainViewModel = new MainViewModel(LoginViewModel);
            LoginViewModel.OnAuthorize += LoginViewModelOnOnAuthorize;
            Window = new MainWindow{DataContext = MainViewModel};
            Window.Show();
        }
        private void LoginViewModelOnOnAuthorize(object sender, LoginEventArgs e)
        {
            if (e.IsAuthorized)
            {
                CustomWindow = new CustomWindow { DataContext = LoginViewModel };
                CustomWindow.Show();
                Window.Close();
                return;
            }
            MainViewModel.Message = $"[{e.User}] {e.Message}";
        }
    }
    

Все, теперь при запуске у нас откроется MainWindow с контентом авторизации и если авторизация успешна, то MainWindow закроется и заместо него появится CustomWindow (другое наше окно).

READ ALSO
Как построить текстурированную сферу в SharpGL и разместить по определенным координатам?

Как построить текстурированную сферу в SharpGL и разместить по определенным координатам?

Хочу нарисовать Землю и Луну с использованием своего алгоритма вращения (те

140
Связать кнопки со счетчиком PHP

Связать кнопки со счетчиком PHP

Скажем у меня есть БД с таблицой users и поля id name countЯ сделал страницу на php ,которая выводит следующую информацию по схеме name count button В моем случае:...

162
Не находит класс (Uncaught Error: Class )

Не находит класс (Uncaught Error: Class )

Всем доброго дня, не могу понять почему не работает (((

138