MyTetra Share
Делитесь знаниями!
Описание MVVM
Время создания: 23.11.2019 20:27
Раздел: Компьютер - C# - WPF - MVVM
Запись: Kozlov-AE/Tetra/master/base/157445530799nd3883ub/text.html на raw.githubusercontent.com

Есть 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;

}

c#  wpf  mvvm  события


Павел Ериков

1971010 бронзовых знаков

Как по мне тут неверно 3 вещи. 1. Вы всю логику размещаете в App, когда он должен быть простым звеном для первичной обработки некой вспомогательной логики самой программы при старте/закрытие. Подписываться там на события не есть хорошо. 2. Вы нарушаете MVVM, по MVVM вы не должны работать с контролами через код, для этого используются VM и привязки. У вас же, на сколько я понял, идет инициализация контрола через код (s = (UserControl1)LoginWindow.Hello.Content;). 3. Frame, тоже не очень верный подход. Как по мне он совершенно не годиться для показа контролов... – EvgeniyZ  27 окт '18 в 22:14

А вы можете поптробнее и ввиде ответа все 3 пункта написать? Заранее благодарю – Павел Ериков  27 окт '18 в 22:23


Если так бегло посмотреть на ваше описание, то у вас тут как минимум 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:
  2. public MainViewModel(LoginViewModel loginVM)

    {

    LoginViewModel = loginVM;

    CurrentContent = LoginViewModel;

    }

  3. Перепишем 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 (другое наше окно).

 
MyTetra Share v.0.65
Яндекс индекс цитирования