Есть 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 проблемы:
- Вы довольно много логики делаете в App, это не очень удачное решение. Мне кажется, что там это даже не логично размещать. Сделайте для таких целей отдельную VM, которая все обработает, зачем же в APP...
- В MVVM нельзя работать с контролами через код, если у вас есть скажем TextBox с именем x:Name="textBox1" и вы хотите задать ему текст таким путем: textBox1.Text = "...", то тут вы напрямую работаете с этим элементом, что по правилам MVVM очень плохо! Для таких целей существую привязки (вы указываете <TextBox Text="{Binding MyTextProperty}">). Попробуйте убрать все имена у контролов, а то и вовсе убрать View часть. По паттерну MVVM ваше приложение должно работать даже без View.
- 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 под них.
- Страница авторизации. На ней мы попросим логин и пароль пользователя, а в ее 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>
- Страница с контентом, который мы покажем после авторизации. Делаем все в точности по аналогии с 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! Вы главное поймите принцип, как все это работает и дальше пойдет все как по маслу! Удачи!
Если у нас разные окна, то самый простой вариант, это сделать следующее:
- Перепишем MainVewModel, а точней его конструктор, пусть он принимает LoginViewModel:
public MainViewModel(LoginViewModel loginVM)
{
LoginViewModel = loginVM;
CurrentContent = LoginViewModel;
}
- Перепишем 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 (другое наше окно).