I'm attempting to create a class & method which could be used on any window and page to change the current page displayed on the MainWindow window.
So far I got:
class MainWindowNavigation : MainWindow
{
public MainWindow mainWindow;
public void ChangePage(Page page)
{
mainWindow.Content = page;
}
}
The main window itself:
public MainWindow()
{
InitializeComponent();
MainWindowNavigation mainWindow = new MainWindowNavigation();
mainWindow.ChangePage(new Pages.MainWindowPage());
}
Unfortunately this ends up with System.StackOverflowException.
The main reason for creating this is that I want to be able to change the mainWindow.Content from a page which is currently displayed in mainWindow.Content.
I have already reviewed MVVM but I don't think it is worth using it for a small application like this as all I want it to do is Display a Welcome Page on open, then on the side there will be few buttons. Once pressed the mainWindow.Content correctly changes to a page where a user can enter login detail and then on the button press on the login page I want to change the mainWindow.Content to a different page on successful validation of the login details entered.
Using MVVM is absolutely fine as it will simplify the implementation of your requirement. WPF is build to be used with the MVVM pattern, which means to make heavy use of data binding and data templates.
The task is quite simple. Create a UserControl (or DataTemplate) for each view e.g., WelcomePage and LoginPage with their corresponding view models WelcomePageViewModel and LoginPageViewModel.
A ContentControl will display the pages.
The main trick is that, when using an implicit DataTemplate (a template resource without an x:Key defined), the XAML parser will automatically lookup and apply the correct template, where the DataType matches the current content type of a ContentControl. This makes navigation very simple, as you just have to select the current page from a collection of page models and set this page via data binding to the Content property of the ContentControl or ContentPresenter:
Usage
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type WelcomePageviewModel}">
<WelcomPage />
</DataTemplate>
<DataTemplate DataType="{x:Type LoginPageviewModel}">
<LoginPage />
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- Page navigation -->
<StackPanel Orientation="Horizontal">
<Button Content="Show Login Screen"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.LoginPage}" />
<Button Content="Show Welcome Screen"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.WelcomePage}" />
</StackPanel>
<!--
Host of SelectedPage.
Automatically displays the DataTemplate that matches the current data type
-->
<ContentControl Content="{Binding SelectedPage}" />
<StackPanel>
</Window>
Implementation
Create the individual page controls (the controls that host the page content). This can be a Control, UserControl, Page or simply a plain DataTemplate:
WelcomePage.xaml
<UserControl>
<StackPanel>
<TextBlock Text="{Binding PageTitle}" />
<TextBlock Text="{Binding Message}" />
</StackPanel>
</UserControl>
LoginPage.xaml
<UserControl>
<StackPanel>
<TextBlock Text="{Binding PageTitle}" />
<TextBox Text="{Binding UserName}" />
</StackPanel>
</UserControl>
Create the page models:
IPage.cs
interface IPage : INotifyPropertyChanged
{
string PageTitel { get; set; }
}
WelcomePageViewModel.cs
class WelcomePageViewModel : IPage
{
private string pageTitle;
public string PageTitle
{
get => this.pageTitle;
set
{
this.pageTitle = value;
OnPropertyChanged();
}
}
private string message;
public string Message
{
get => this.message;
set
{
this.message = value;
OnPropertyChanged();
}
}
public WelcomePageViewModel()
{
this.PageTitle = "Welcome";
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
LoginPageViewModel.cs
class LoginPageViewModel : IPage
{
private string pageTitle;
public string PageTitle
{
get => this.pageTitle;
set
{
this.pageTitle = value;
OnPropertyChanged();
}
}
private string userName;
public string UserName
{
get => this.userName;
set
{
this.userName = value;
OnPropertyChanged();
}
}
public LoginPageViewModel()
{
this.PageTitle = "Login";
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Create an enumeration of page identifiers (to eliminate magic strings in XAML and C#):
PageName.cs
public enum PageName
{
Undefined = 0, WelcomePage, LoginPage
}
Create the MainViewModel which will manage the pages and their navigation:
MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class MainViewModel
{
public ICommand SelectPageCommand => new RelayCommand(SelectPage);
private Dictionary<PageName, IPage> Pages { get; }
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
this.selectedPage = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
this.Pages = new Dictionary<PageName, IPage>
{
{ PageName.WelcomePage, new WelcomePageViewModel() },
{ PageName.LoginPage, new LoginPageViewModel() }
};
this.SelectedPage = this.Pages.First().Value;
}
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
this.SelectedPage = selectedPage;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
You probably want to define MainWindowNavigation as a static class with a method that simply changes the Content of the current MainWindow:
static class MainWindowNavigation
{
public static void ChangePage(Page page)
{
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
if (mainWindow != null)
mainWindow.Content = page;
}
}
You can then call the method from any class without having a reference to the MainWindow:
MainWindowNavigation.ChangePage(new Pages.MainWindowPage());
Related
Question
How can I change the visibility of my hamburger button by not using shellviewmodel ?
P.s. : Its an UWP app working on template10.
Is it possible to bind the Hamburger button visibility to two different Viewmodels in template10?
Shell.xaml
xmlns:vm="using:ScanWorx.ViewModels"
xmlns:converters="using:ScanWorx.ViewModels"
<Page.DataContext>
<vm:ShellViewModel x:Name="ViewModel" />
</Page.DataContext>
<Page.Resources>
<converters:BooleanToVisibilityConverter x:Key="Converter" />
</Page.Resources>
<controls:HamburgerMenu x:Name="MyHamburgerMenu">
<controls:HamburgerMenu.PrimaryButtons>
<!-- HeatMap Generator Button -->
<controls:HamburgerButtonInfo ClearHistory="True" PageType="views:HeatMapGeneratorPage"
Visibility="{x:Bind vm:LoginPageViewModel.abc, Mode=TwoWay,
Converter={StaticResource Converter}}">
<StackPanel Orientation="Horizontal">
<SymbolIcon
Width="48"
Height="48"
Symbol="ViewAll" />
<TextBlock
Margin="12,0,0,0"
VerticalAlignment="Center"
Text="HeatMap Generator" />
</StackPanel>
</controls:HamburgerButtonInfo>
</controls:HamburgerMenu.PrimaryButtons>
</controls:HamburgerMenu>
LoginPageViewModel.cs
namespace ScanWorx.ViewModels
{
class LoginPageViewModel : ViewModelBase
{
public static bool abc { get; set; }
public static Visibility visibility1 { get; set; }
public async void Button_Login_Click()
{
try
{
*code to decrypt my login details and then after decryptng i check for next condition*
if (ApplicationData.Current.LocalSettings.Values["UserAccessLevel"].ToString() == "Administrator")
{
abc = true;
visibility1 = Visibility.Visible;
}
else
{
abc = false;
visibility1 = Visibility.Collapsed;
}
}
}
}
}
ShellViewModel.cs :
If i use shellviewModel in binding The hiding works fine by changing - Visibility="{x:Bind vm:ShellViewModel.ShowButton} , but with this it can only be hard coded either false or true making it visible or collapsed once when app starts.
But i dont want to change the visibility of my button with shell view model i want to change it after i log in with the LoginPageViewModel*
namespace ScanWorx.ViewModels
{
class ShellViewModel : ViewModelBase
{
public static bool ShowButton { get; set; }
public ShellViewModel()
{
ShowButton = false;
}
}
}
How to bind a visibility of HamburgerButtonInfo to another viewmodel other than ShellViewModel?
The better way is make a global setting class to record if login. After login success, edit the setting class ShowButton bool property to true.
Setting.cs
public class Setting : INotifyPropertyChanged
{
private bool _showBtn = false;
public bool ShowBtn
{
get { return _showBtn; }
set { _showBtn = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Xaml bind
<Application.Resources>
<ResourceDictionary>
<local:Setting x:Key="Setting"/>
</ResourceDictionary>
</Application.Resources>
......
Visibility="{Binding ShowBtn, Source={StaticResource Setting}}"
Value Change
((Setting)Application.Current.Resources["Setting"]).ShowBtn = true;
I made a Memory Game with a start menu, and a separate page where the game takes place. On the main menu you can select your theme and press a button to go to the game. I also have a button on the game screen, but I can not figure out how to make it link to the main menu.
I honestly don't know how to go about doing this.
This is the button that links from the main page to the game(in the xaml):
<Button DockPanel.Dock="Top" Padding="25" Click="Play_Clicked" Background="#FF0E0E0E" Foreground="#FFF3FF00" FontSize="18">Start Game</Button>
this is the code in xaml.cs:
private void Play_Clicked(object sender, RoutedEventArgs e)
{
var startMenu = DataContext as StartMenuViewModel;
startMenu.StartNewGame(categoryBox.SelectedIndex);
}
This is the code from "StartMenuViewModel" that contains the "StartNewGame":
public void StartNewGame(int categoryIndex)
{
var category = (SlideCategories)categoryIndex;
GameViewModel newGame = new GameViewModel(category);
_mainWindow.DataContext = newGame;
}
This code works, does anyone know how to make a similar button to go from the game screen to the main menu?
The easiest and most lightweight way opposed to using a Frame, is to create a view model for each page. Then create a main view model which holds all pages and manages their selection. A ContentControl will display the view models using a DataTemplate assigned to the ContentControl.ContentTemplate property or in a multi page scenario either a DataTemplateSelector assigned to ContentControl.ContentTemplateSelector or implicit templates by only defining the DataTemplate.DataType without the Key attribute:
The View
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel x:Key="MainViewModel" />
</Window.DataContext>
<Window.Resources>
<!--
The templates for the view of each page model.
Can be moved to dedicated files.
-->
<DataTemplate DataType="{x:Type PageA}">
<Border Background="Coral">
<TextBlock Text="{Binding Title}" />
</Border>
</DataTemplate>
<DataTemplate DataType="{x:Type PageB}">
<Border Background="DeepSkyBlue">
<TextBlock Text="{Binding Title}" />
</Border>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="Load Page A"
Command="{Binding SelectPageFromIndexCommand}"
CommandParameter="0" />
<Button Content="Load Page B"
Command="{Binding SelectPageFromIndexCommand}"
CommandParameter="1" />
<!-- The actual page control -->
<ContentControl Content="{Binding SelectedPage}" />
</StackPanel>
</Window>
The View Model
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
this.Pages = new ObservableCollection<IPage>() {new PageA() {Title = "Page A"}, new PageB() {Title = "Page B"}};
// Show startup page
this.SelectedPage = this.Pages.First();
}
// Define the Execute and CanExecute delegates for the command
// and pass to constructor
public ICommand SelectPageFromIndexCommand => new SelectPageCommand(
param => this.SelectedPage = this.Pages.ElementAt(int.Parse(param as string)),
param => int.TryParse(param as string, out int index));
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
if (object.Equals(value, this.selectedPage))
{
return;
}
this.selectedPage = value;
OnPropertyChanged();
}
}
private ObservableCollection<IPage> pages;
public ObservableCollection<IPage> Pages
{
get => this.pages;
set
{
if (object.Equals(value, this.pages))
{
return;
}
this.pages = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
SelectPageCommand.cs
class SelectPageCommand : ICommand
{
public SelectPageCommand(Action<object> executeDelegate, Predicate<object> canExecuteDelegate)
{
this.ExecuteDelegate = executeDelegate;
this.CanExecuteDelegate = canExecuteDelegate;
}
private Predicate<object> CanExecuteDelegate { get; }
private Action<object> ExecuteDelegate { get; }
#region Implementation of ICommand
public bool CanExecute(object parameter) => this.CanExecuteDelegate?.Invoke(parameter) ?? false;
public void Execute(object parameter) => this.ExecuteDelegate?.Invoke(parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
#endregion
}
The Page Models
IPage.cs
// Base type for all pages
interface IPage : INotifyPropertyChanged
{
string Title { get; set; }
}
PageA.cs
// IPage implementation.
// Consider to introduce dedicated interface IPageA which extends IPage
class PageA : IPage
{
public string Title { get; set; }
// Implementation of INotifyPropertyChanged
}
PageB.cs
// IPage implementation.
// Consider to introduce dedicated interface IPageB which extends IPage
class PageB : IPage
{
public string Title { get; set; }
// Implementation of INotifyPropertyChanged
}
I have the below problem: I have two different user controls inside a parent user control. These are trainList, which holds a list of train objects and trainView, which is an user control that shows details of the selected train in the list.
My wish is to share a variable of trainList with trainView.
What I have now is:
Parent user control:
<UserControl>
<UserControl>
<customControls:trainList x:Name="trainList"></customControls:trainList>
</UserControl>
<UserControl>
<customControls:trainView x:Name="trainView"></customControls:trainView>
</UserControl>
<TextBlock DataContext="{Binding ElementName=trainList, Path=SelectedTrain}" Text="{ Binding SelectedTrain.Id }">Test text</TextBlock>
</UserControl>
TrainList class:
public partial class TrainList : UserControl
{
public TrainList()
{
this.InitializeComponent();
this.DataContext = this;
}
public Train SelectedTrain { get; set; }
public void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.Print(this.SelectedTrain.Id);
}
}
Note: The Train class implements INotifyPropertyChanged.
If I got this to work, I'd apply the binding to the trainView user control (not sure if this would work) instead to the text block.
<UserControl>
<customControls:trainView x:Name="trainView" DataContext="{Binding ElementName=trainList, Path=SelectedTrain}"></customControls:trainView>
</UserControl>
And then, I would access that variable someway from the code-behind of trainView.
(And after this, I would like to share a different variable from trainView with its parent user control, but maybe that's another question).
My current question is: could this be done this way or would I need to follow another strategy?
Take this simple view model, with a base class that implements the INotifyPropertyChanged interface, and a Train, TrainViewModel and MainViewModel class.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(
ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (!Equals(storage, value))
{
storage = value;
OnPropertyChanged(propertyName);
}
}
}
public class Train : ViewModelBase
{
private string name;
public string Name
{
get { return name; }
set { SetValue(ref name, value); }
}
private string details;
public string Details
{
get { return details; }
set { SetValue(ref details, value); }
}
// more properties
}
public class TrainViewModel : ViewModelBase
{
public ObservableCollection<Train> Trains { get; }
= new ObservableCollection<Train>();
private Train selectedTrain;
public Train SelectedTrain
{
get { return selectedTrain; }
set { SetValue(ref selectedTrain, value); }
}
}
public class MainViewModel
{
public TrainViewModel TrainViewModel { get; } = new TrainViewModel();
}
which may be initialized in the MainWindow's constructor like this:
public MainWindow()
{
InitializeComponent();
var vm = new MainViewModel();
DataContext = vm;
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 1",
Details = "Details of Train 1"
});
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 2",
Details = "Details of Train 2"
});
}
The TrainDetails controls would look like this, of course with more elements for more properties of the Train class:
<UserControl ...>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Details}"/>
</StackPanel>
</UserControl>
and the parent UserControl like this, where I directly use a ListBox instead of a TrainList control:
<UserControl ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding Trains}"
SelectedItem="{Binding SelectedTrain}"
DisplayMemberPath="Name"/>
<local:TrainDetailsControl Grid.Column="1" DataContext="{Binding SelectedTrain}"/>
</Grid>
</UserControl>
It would be instantiated in the MainWindow like this:
<Grid>
<local:TrainControl DataContext="{Binding TrainViewModel}"/>
</Grid>
Note that in this simple example the elements in the UserControls' XAML bind directly to a view model instance that is passed via their DataContext. This means that the UserControl know the view model (or at least their properties). A more general approach is to declare dependency properties in the UserControl class, that are bound to view model properties. The UserControl would then be independent of any particular view model.
I'm currently in the process of mastering the C# WPF MVVM pattern and have stumbled upon a pretty big hurdle...
What I am trying to do fire off a LoginCommand that when successfully executed will allow me to change the parent window's viewmodel. The only issue is I can't quite think of a way to change the parent window's viewmodel without breaking the MVVM design pattern because I can't access the parent window's ContentControl that sets its path to the active UserControlViewModel in the window.
Here's the scenario:
In our App.xaml we have two DataTemplates:
<DataTemplate DataType="{x:Type ViewModels:LoginViewModel}">
<Views:LoginView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:LoggedInViewModel}">
<Views:LoggedView />
</DataTemplate>
In our MainWindow we have:
<ContentControl Content="{Binding ViewModel}" />
The MainWindow code behind will set the ViewModel = LoginViewModel
In our LoginViewModel we have:
<Button Command="{Binding LoginCommand}" CommandParameter="{Binding ElementName=pwPasswordBoxControlInXaml}" />
Now for the money... the LoginCommand:
public void Execute(object parameter)
{
// Do some validation
// Async login task stuff
// ...
// Logged in... change the MainWindow's ViewModel to the LoggedInViewModel
}
How can I make the Execute method change the window's viewmodel without breaking the MVVM pattern?
Things I've tried thus far:
Making the MainWindow have a static Instance singleton that I can access and then change the ViewModel property from the command.
Attempting to implement some form of routed command listener in the MainWindow and then have commands fire off routed command events to be handled by the parent window.
I've done a quick demo to show one way of doing it. I've kept it as simple as possible to give the general idea. There are lots of different ways of accomplishing the same thing (e.g. you could hold a reference to MainWindowViewModel inside LoginViewModel, handle everything there then call a method on MainWindowViewModel to trigger the workspace change, or you could use Events/Messages, etc).
Definitely have a read of Navigation with MVVM though. That's a really good introduction that I found helpful when I was getting started with it.
The key thing to take away from this is to have an outer MainWindowViewModel or ApplicationViewModel which handles the navigation, holds references to workspaces, etc. Then the choice of how you interact with this is up to you.
In the code below, I've left out the clutter from defining Window, UserControl, etc. to keep it shorter.
Window:
<DockPanel>
<ContentControl Content="{Binding CurrentWorkspace}"/>
</DockPanel>
MainWindowViewModel (this should be set as the DataContext for the Window):
public class MainWindowViewModel : ObservableObject
{
LoginViewModel loginViewModel = new LoginViewModel();
LoggedInViewModel loggedInViewModel = new LoggedInViewModel();
public MainWindowViewModel()
{
CurrentWorkspace = loginViewModel;
LoginCommand = new RelayCommand((p) => DoLogin());
}
private WorkspaceViewModel currentWorkspace;
public WorkspaceViewModel CurrentWorkspace
{
get { return currentWorkspace; }
set
{
if (currentWorkspace != value)
{
currentWorkspace = value;
OnPropertyChanged();
}
}
}
public ICommand LoginCommand { get; set; }
public void DoLogin()
{
bool isValidated = loginViewModel.Validate();
if (isValidated)
{
CurrentWorkspace = loggedInViewModel;
}
}
}
LoginView:
In this example I'm binding a Button on the LoginView to the LoginCommand on the Window DataContext (i.e. MainWindowViewModel).
<StackPanel Orientation="Vertical">
<TextBox Text="{Binding UserName}"/>
<Button Content="Login" Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.LoginCommand}"/>
</StackPanel>
LoginViewModel:
public class LoginViewModel : WorkspaceViewModel
{
private string userName;
public string UserName
{
get { return userName; }
set
{
if (userName != value)
{
userName = value;
OnPropertyChanged();
}
}
}
public bool Validate()
{
if (UserName == "bob")
{
return true;
}
else
{
return false;
}
}
}
LoggedInView:
<StackPanel Orientation="Vertical">
<TextBox Text="{Binding RestrictedData}"/>
</StackPanel>
LoggedInViewModel:
public class LoggedInViewModel : WorkspaceViewModel
{
private string restrictedData = "Some restricted data";
public string RestrictedData
{
get { return restrictedData; }
set
{
if (restrictedData != value)
{
restrictedData = value;
OnPropertyChanged();
}
}
}
}
WorkspaceViewModel:
public abstract class WorkspaceViewModel : ObservableObject
{
}
Then some other classes you probably already have implemented (or alternatives).
ObservableObject:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
}
RelayCommand:
public class RelayCommand : ICommand
{
private readonly Action<object> execute;
private readonly Predicate<object> canExecute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{ }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return canExecute == null ? true : canExecute(parameter);
}
public void Execute(object parameter)
{
execute(parameter);
}
}
App.Xaml:
<DataTemplate DataType="{x:Type ViewModels:LoginViewModel}">
<Views:LoginView />
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:LoggedInViewModel}">
<Views:LoggedInView />
</DataTemplate>
<ContentControl Content="{Binding ViewModel}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type vm:LoginViewModelClass}">
<!-- some LoginView -->
</DataTemplate>
<DataTemplate DataType="{x:Type vm:LoggedInViewModelClass}">
<!-- some LoggedInView -->
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.