I have been attempting to implement a Sudoku game within a WPF application I am making. I found the following site that pretty much gave me the perfect starting point to try and add the sudoku to my app.
There is however a major difference which I didn't think much of at first. The code from this site bases everything off a single Window, no UserControls at all, which in itself isn't an issue. However, my current implementation bases the whole content of the app on a ContentControl element.
To skip useless details, here is my MainWindow.xaml file (with everything unrelated to the issue removed):
<Window x:Class="BasicGameApp.MainWindow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BasicGameApp.MainWindow"
xmlns:viewModel="clr-namespace:BasicGameApp.MainWindow.MVVM.ViewModel"
mc:Ignorable="d"
Height="700"
Width="1080"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="NoResize"
Background="Transparent"
AllowsTransparency="True">
<Window.DataContext>
<viewModel:MainViewModel/>
</Window.DataContext>
<Border>
<Grid>
<ContentControl Grid.Row="1"
Grid.Column="1"
Margin="10"
Content="{Binding CurrentView}"/>
</Grid>
</Border>
</Window>
I based my UI on this YouTube tutorial if anyone is curious.
The MainViewModel.cs looks like this:
namespace BasicGameApp.MainWindow.MVVM.ViewModel
{
class MainViewModel : ObservableObject
{
#region Commands
public RelayCommand HomeViewCommand { get; set; }
public RelayCommand SudokuViewCommand { get; set; }
#endregion
#region ViewModels
public HomeViewModel HomeVM { get; set; }
public SudokuViewModel SudokuVM { get; set; }
#endregion
private object _currentView;
public object CurrentView
{
get => _currentView;
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
HomeVM = new HomeViewModel();
SudokuVM = SudokuViewModel.GetInstance(new SudokuView());
HomeViewCommand = new RelayCommand(o =>
{
CurrentView = HomeVM;
});
SudokuViewCommand = new RelayCommand(o =>
{
CurrentView = SudokuVM;
});
}
}
}
And here are the ObservableObject RelayCommand classes:
namespace BasicGameApp.MainWindow.Core
{
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class RelayCommand : ICommand
{
private Action<object> _execute;
private Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
}
As you can see, everything is programmed correctly and works as intended EXCEPT, the SudokuViewModel. As you can see from the code, the SudokuViewModel isn't instantiated as a public class (this is from the tutorial I stated at the beginning). From that tutorial, the instantiation of the view and associated viewmodel is done as follows in the App.xaml.cs:
public partial class App : Application
{
public void ApplicationStartup(object sender, StartupEventArgs args)
{
MainWindow mainWindow = new MainWindow(); // Instantiate the main window
mainWindow.ViewModel = ViewModelClass.GetInstance(mainWindow); // Get an instance of the ViewModel and set the View's ViewModel pointer
mainWindow.Show(); // Now display the view
}
}
My App.xaml file isn't empty however and contains the following:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/BasicGameApp.MainWindow;component/Themes/Generic.xaml"/>
</ResourceDictionary.MergedDictionaries>
<DataTemplate DataType="{x:Type viewModel:HomeViewModel}">
<view:HomeView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModel:SudokuViewModel}">
<view:SudokuView/>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
Needless to say that the difference between these two setups makes things rather difficult. I attempted to change the DataTemplate to set the ViewModel property on the SudokuView but I can't seem to get it to Bind correctly:
<view:SudokuView ViewmModel="{Binding //The calling ViewModel}"/>
I'm currently out of ideas as to how I can either adapt the sudoku code to work "without" a viewmodel or adapt the DataTemplate to provide the view with the SudokuViewModel.
Please bare in mind that I am initially an Android developper and this is simply to learn new skills on a personal level.
Related
i am new to wpf and xaml and try to change the content of a window (Login -> Main content and main content -> Login) in an WindowsApplication (Xaml, WPF). So far i have the following for this simple login/logout scenario:
BaseViewModel
public class BaseViewModel : DependencyObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
BaseMainViewViewModel (Base class for setting the MainViewType Property in the MainWindow. It also contains the command to change the property via the button in the MainViews.)
public class BaseMainViewViewModel : BaseViewModel
{
private static MainViewType _CurrentMainView;
private ICommand _SwitchMainViewCommand;
public BaseMainViewViewModel()
{
SwitchMainViewCommand = new RelayCommand(SwitchMainView);
}
public MainViewType CurrentMainView
{
get { return _CurrentMainView; }
set
{
if (value != _CurrentMainView)
{
_CurrentMainView = value;
OnPropertyChanged(nameof(CurrentMainView));
}
}
}
public ICommand SwitchMainViewCommand
{
get { return _SwitchMainViewCommand; }
set { _SwitchMainViewCommand = value; }
}
#region Test
public void SwitchMainView(object param)
{
Debugger.Break();
switch (CurrentMainView)
{
case MainViewType.Login:
CurrentMainView = MainViewType.Main;
break;
case MainViewType.Main:
CurrentMainView = MainViewType.Login;
break;
default:
break;
}
MessageBox.Show("Login/Logout");
}
#endregion Test
LoginViewModel inherites from BaseMainViewViewModel to get access to the CurrentMainView-Property
public class LoginViewModel : BaseMainViewViewModel {}
MainViewModel her the same
public class MainViewModel : BaseMainViewViewModel {}
MainWindowViewModel
public class MainWindowViewModel: BaseMainViewViewModel {}
LoginMainView
public partial class LoginMainView : UserControl
{
public LoginMainView()
{
InitializeComponent();
DataContext = new LoginViewModel();
}
}
Currently i have only one button (Login-Button) in the LoginMainView. If I click this button, the current LoginMainView should be exchanged with the MainMainView.
<Grid>
<Button Content="Main" Background="Red" Command="{Binding SwitchMainViewCommand}" />
</Grid>
MainMainView
public partial class MainMainView : UserControl
{
public LoginMainView()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
here the same (Logout-Button) correspond to LoginMainView...
<Grid>
<Button Content="Logout" Background="Green" Command="{Binding SwitchMainViewCommand}" />
</Grid>
MainWindow
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
In the MainWindow-View i bind the CurrentMainView-Property (MainViewType) from the BaseMainViewViewModel to the contentpresenter, which i will change by clicking the button in the MainMainView/LoginMainView and the ValueConverter shold do the rest.
<Grid>
<StackPanel>
<Label Content="Test" />
<ContentPresenter Content="{Binding CurrentMainView, Converter={view:MainViewValueConverter}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
MainViewType
public enum MainViewType
{
Login = 0,
Main = 1
}
BaseValueConverter
public abstract class BaseValueConverter<T> : MarkupExtension, IValueConverter
where T : class, new()
{
private static T _Converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _Converter ?? (_Converter = new T());
}
public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
public abstract object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
RelayCommand
public class RelayCommand : ICommand
{
private Action<object> _Execute;
private Predicate<object> _CanExecute;
private event EventHandler CanExecuteChangedInternal;
public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute) { }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_Execute = execute ?? throw new ArgumentNullException("execute");
_CanExecute = canExecute ?? throw new ArgumentNullException("canExecute");
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
CanExecuteChangedInternal += value;
}
remove
{
CommandManager.RequerySuggested -= value;
CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter)
{
return (_CanExecute != null) && _CanExecute(parameter);
}
public void Execute(object parameter)
{
_Execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler eventHandler = CanExecuteChangedInternal;
if (eventHandler != null)
{
eventHandler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
_CanExecute = _ => false;
_Execute = _ => { return; };
}
private static bool DefaultCanExecute(object parameter)
{
return true;
}
}
When i start the application, the ValueConverter is called and the correct View (LoginMainView) is loaded. I then click on the button in the LoginMainView, the command (SwitchMainView) is executed, but then the content of MainWindow is not changed into MainMainView because the ValueConverter is not used.
What am i doing wrong? Do i have a fundamental understanding problem? Or is it not possible in this way to map the simple login/logout scenario? Or did i simply overlook something? Can someone please tell me what i have forgotten?
Many thanks in advance to the helpers!
You don't need ValueConverter for that. You are on a right track thoug. Take a look here - this is sample application for ReactiveUI framework (which is my favourite).
It has AppBootrsapper (ViewModel of the application). As the framework does some magick around it, the basic idea is:
MainWindow.Xaml:
<Window x:Class="ReactiveUI.Samples.Routing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rx="clr-namespace:ReactiveUI;assembly=ReactiveUI"
Title="MainWindow" Height="350" Width="525">
<Grid UseLayoutRounding="True" >
<ContentControl Content="{Binding ActiveViewModel}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type LoginViewModel}">
<!-- here you put your content wof login screen, prefereably as seperate UserControl -->
</DataTemplate>
<DataTemplate DataType="{x:Type MainViewModel}">
<!-- here you put your content main screen, prefereably as seperate UserControl -->
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Window>
Then you just set AppBootstrapper.ActiveViewModel = new LoginViewModel() and you have login screen.
If you login, AppBootstrapper.ActiveViewModel = new MainViewModel() and WPF displays main screen.
All that and much more is done by ReactiveUI framwork - only there instead of putting DataTemplates for ViewModels, you register UserControls as views and RoutedViewHost does all the magick. Don't do that on your own, it's inventing the wheel again.
EDIT to answer the comment:
You put AppBootstrapper.ActiveViewModel = new MainViewModel() in your NavigationService. Navigation meaning the thing that changes displayed view. Most common version is a stack, which top is active ViewModel. When you press Back button, you just pop the stack.
This all applies to MVVM model with Model First navigation, which means you first instantiate ViewModel, and navigation service finds the proper view.
You can do this in the other way: View First navigation. There are some tutorials for WPF page navigation. It works exactly the same, but instead of ViewModel, you create a page (a view) which then creates underlying data.
MVVM app model is so popular, because it allows very clean logic and presentation separation (XAML is ONLY about view, ViewModels contain all logic, Models persist the data), which in turn makes it very easy to share logic between platforms. In fact, if you do that correctly, you can use all your ViewModels in apps written in Xamarin, WPF or UWP, just by creating platform-specific views.
To wrap up, WPF allows you to switch in a property data and it will find a view for it automatically (via DataTemplates). Remember about INotifyPropertyChanged and everything will work
I am trying to learn WPF/MVVM and for educational reason I create a simple application. I have some issues trying to implement a Command Object.
When a button control is clicked I want the background color of the Grid change to yellow using a Command Object. There are a lot of stuff about how to do this, but I want to do it with the clean way. Generally I want to achieve a loose coupling between View, ViewModel and the Command Object in order to test those classes.
Also i do not want to use some Libraries like Prism because I have the need to fully understand MVVM first.
I have a code sample but of course it does not have functionality. Just represented it for convenience reason.
My view XAML
<Window x:Class="Calendar.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Calendar"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="480">
<Grid Background="{Binding BackgroundColour}" Margin="0,0,2,0">
<Button Margin="197,247,200,-239" Grid.Row="3" Grid.ColumnSpan="2" Command="{Binding SubmitCommand}">Color</Button>
</Grid>
My ModelView class
public class MainWindowViewModel : INotifyPropertyChanged {
//Command part
ICommand SubmitCommand;
public MainWindowViewModel(ICommand command) {
SubmitCommand = command;
}
//Data Binding part
public event PropertyChangedEventHandler PropertyChanged;
private Brush backgroundColour = (Brush)new BrushConverter().ConvertFromString("Red");
public Brush BackgroundColour {
get { return this.backgroundColour; }
set {
if (value != this.backgroundColour) {
this.backgroundColour = value;
var handler = this.PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs("BackgroundColour"));
}
}
}
(it also has a data binding part but it does not have to do with my issue)
You would like not to have anything related to windows like colors(Brushes or Brush) in the viewmodel. Refer my below code.
<Window x:Class="MVVMNav_Learning.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MVVMNav_Learning"
mc:Ignorable="d"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<local:ColorConverterConverter x:Key="ColorConverterConverter"></local:ColorConverterConverter>
</Window.Resources>
<Grid>
<Grid Background="{Binding BackgroundColour,Converter={StaticResource ColorConverterConverter}}" Margin="0,0,2,0">
<Button Margin="50" Command="{Binding SubmitCommand}">Color</Button>
</Grid>
</Grid>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel:INotifyPropertyChanged
{
private MyColor backColor;
public MyColor BackgroundColour
{
get { return backColor; }
set { backColor = value; OnPropertyChanged("BackgroundColour"); }
}
public ICommand SubmitCommand { get; set; }
public ViewModel()
{
BackgroundColour = MyColor.Red;
SubmitCommand = new BaseCommand(Execute);
}
public void Execute(object parameter)
{
BackgroundColour = MyColor.Yellow;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
public enum MyColor
{
Red,
Green,
Yellow
}
public class BaseCommand : ICommand
{
private Action<object> _method;
public event EventHandler CanExecuteChanged;
public BaseCommand(Action<object> method)
{
_method = method;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_method.Invoke(parameter);
}
}
public class ColorConverterConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
MyColor color = (MyColor)value;
switch (color)
{
case MyColor.Red:
return Brushes.Red;
case MyColor.Green:
return Brushes.Green;
case MyColor.Yellow:
return Brushes.Yellow;
default:
{
return Brushes.Red;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You need to create a public Property for the ICommand SubmitCommand and you can use a private DelegateCommand in its getter/setter.
You are not very clearly stating your question, but I gamble it to be: How to configure the command parameter for the viewmodel's constructor to have it change the backgroundcolour?
Commands do their work by having them implement ICommand.Execute(Object) So basically you want to have the command object you pass to the constructor to have a method like:
void Execute(object parameter)
{
viewModel.BackGroundColor=Brushes.Yellow;
}
This is awkward: the command is passed from outside the viewmodel, but it must have a reference to it to change its back colour. You may want to rethink your design.
Moreover: for the databinding engine to see the SubmitChangedCommand it must be a property:
public ICommand SubmitChangesCommand {get;set;}
Current Situation
I am using the following approach to resolve a View for a matching ViewModel. (simplified)
<Window.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type local:DemoVm2}">
<local:DemoViewTwo />
</DataTemplate>
<DataTemplate DataType="{x:Type local:DemoVm}">
<local:DemoView />
</DataTemplate>
</ResourceDictionary>
</Window.Resources>
<DockPanel LastChildFill="True">
<Button Content="Switch To VmOne" Click="ButtonBase_OnClick"></Button>
<Button Content="Switch To VmTwo" Click="ButtonBase_OnClick2"></Button>
<ContentPresenter Content="{Binding CurrentContent}" />
</DockPanel>
The Views get automatically resolved by WPF after switching the ViewModel inside the ContentPresenter.
When using complex View’s that may take 2-4 seconds for initialization I want to display a BusyIndicator. They take up to 2-4 seconds because of the amount of visuals NOT data.
Problem
I don’t know when the View’s have finished their initialization/loading process because I only have access to the current ViewModel.
My approach
My Idea was to attach a behavior to each UserControl that may set a boolean Value to the their attached ViewModel (IsBusy=false) after InitializeComponent() finished or handle their LoadedEvent. This Property could be bound to a BusyIndicator elsewhere.
I am not really satisfied with this solution because I would need to attach this behavior to each individual Usercontrol/view.
Does anyone have another solution for this kind of problem? I guess I am not the only one who wants to hide the GUI loading process from the user?!
I recently came across this Thread http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx . But since this is from 2007 there might be some better / more conveniant ways to achieve my goal?
There is no easy and universal solution for this problem.
In each concrete case you should write custom logic for non blocking visual tree initialization.
Here is an example how to implement non blocking initialization of ListView with initializing indicator.
UserControl that contains ListView and initializing indicator:
XAML:
<UserControl x:Class="WpfApplication1.AsyncListUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Margin="5" Grid.Row="1">
<ListView x:Name="listView"/>
<Label x:Name="itemsLoadingIndicator" Visibility="Collapsed" Background="Red" HorizontalAlignment="Center" VerticalAlignment="Center">Loading...</Label>
</Grid>
</UserControl>
CS:
public partial class AsyncListUserControl : UserControl
{
public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(IEnumerable), typeof(AsyncListUserControl), new PropertyMetadata(null, OnItemsChanged));
private static void OnItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AsyncListUserControl control = d as AsyncListUserControl;
control.InitializeItemsAsync(e.NewValue as IEnumerable);
}
private CancellationTokenSource _itemsLoadiog = new CancellationTokenSource();
private readonly object _itemsLoadingLock = new object();
public IEnumerable Items
{
get
{
return (IEnumerable)this.GetValue(ItemsProperty);
}
set
{
this.SetValue(ItemsProperty, value);
}
}
public AsyncListUserControl()
{
InitializeComponent();
}
private void InitializeItemsAsync(IEnumerable items)
{
lock(_itemsLoadingLock)
{
if (_itemsLoadiog!=null)
{
_itemsLoadiog.Cancel();
}
_itemsLoadiog = new CancellationTokenSource();
}
listView.IsEnabled = false;
itemsLoadingIndicator.Visibility = Visibility.Visible;
this.listView.Items.Clear();
ItemsLoadingState state = new ItemsLoadingState(_itemsLoadiog.Token, this.Dispatcher, items);
Task.Factory.StartNew(() =>
{
int pendingItems = 0;
ManualResetEvent pendingItemsCompleted = new ManualResetEvent(false);
foreach(object item in state.Items)
{
if (state.CancellationToken.IsCancellationRequested)
{
pendingItemsCompleted.Set();
return;
}
Interlocked.Increment(ref pendingItems);
pendingItemsCompleted.Reset();
state.Dispatcher.BeginInvoke(
DispatcherPriority.Background,
(Action<object>)((i) =>
{
if (state.CancellationToken.IsCancellationRequested)
{
pendingItemsCompleted.Set();
return;
}
this.listView.Items.Add(i);
if (Interlocked.Decrement(ref pendingItems) == 0)
{
pendingItemsCompleted.Set();
}
}), item);
}
pendingItemsCompleted.WaitOne();
state.Dispatcher.Invoke(() =>
{
if (state.CancellationToken.IsCancellationRequested)
{
pendingItemsCompleted.Set();
return;
}
itemsLoadingIndicator.Visibility = Visibility.Collapsed;
listView.IsEnabled = true;
});
});
}
private class ItemsLoadingState
{
public CancellationToken CancellationToken { get; private set; }
public Dispatcher Dispatcher { get; private set; }
public IEnumerable Items { get; private set; }
public ItemsLoadingState(CancellationToken cancellationToken, Dispatcher dispatcher, IEnumerable items)
{
CancellationToken = cancellationToken;
Dispatcher = dispatcher;
Items = items;
}
}
}
Usage example:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Load Items" Command="{Binding LoadItemsCommand}" />
<local:AsyncListUserControl Grid.Row="1" Items="{Binding Items}"/>
</Grid>
</Window>
ViewModel:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
public class MainWindowViewModel:INotifyPropertyChanged
{
private readonly ICommand _loadItemsCommand;
private IEnumerable<string> _items;
public event PropertyChangedEventHandler PropertyChanged;
public MainWindowViewModel()
{
_loadItemsCommand = new DelegateCommand(LoadItemsExecute);
}
public IEnumerable<string> Items
{
get { return _items; }
set { _items = value; OnPropertyChanged(nameof(Items)); }
}
public ICommand LoadItemsCommand
{
get { return _loadItemsCommand; }
}
private void LoadItemsExecute(object p)
{
Items = GenerateItems();
}
private IEnumerable<string> GenerateItems()
{
for(int i=0; i<10000; ++i)
{
yield return "Item " + i;
}
}
private void OnPropertyChanged(string propertyName)
{
var h = PropertyChanged;
if (h!=null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute) : this(execute, null) { }
public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
}
}
The main features of this approach:
Custom dependency properties for data that requires much UI
initialization.
DependencyPropertyChanged callback starts worker thread that manages
UI initialization.
Worker thread dispatches small actions with low execution priority
into UI thread which keeps UI responsible.
Additional logic to keep consistent state in case when
initialization executed again while previous initialization is not
completed yet.
An alternate approach is to start with UserControl hidden and IsBusy on true. Start the loading in a separate thread on the Application.Dispatcher. The final statements of the tread are IsBusy=false; UserControl.Visibility = Visibility.Visible;
So I have a View with two subviews. One of the subviews is an on screen keyboard with textbox. Below that are some buttons which are part of a different subview. See below:
When I press the keyboard buttons it types in the textbox. Both the subview with the buttons and the subview with the keyboard have their own ViewModels. My question is, how do I reference the keyboard view from the button view (so I can get the contents of the text field, for example, or clear it if the user clicks "Go Back").
I'm trying to conceptualize it, but I can't figure out how I would get the same instance of the ViewModel of the keyboard that the Main View has.
I can create a variable:
private KeyboardViewModel keyboard;
But how do I instantiate that variable with the instance that the Main View already has (so I can access those properties from the button viewmodel)?
The main problem is that you misplaced your datasource in one of your ViewModel when the datasource is actually needed to be reuse in multiple View/ViewModel. What you need to do is to refactor the datasource out into a singleton instance or an seperate instance that can be injected into different ViewModels' constructor. By decoupling out the datasource from a particular ViewModel can give it freedom for different place to access.
public class DataCache
{
private static DataCache singletonInstance;
// You can have freedom to choose the event-driven model here
// Using traditional Event, EventAggregator, ReactiveX, etc
public EventHandler OnMessageChanged;
private DataCache()
{
}
public static DataCache Instance
{
get { return singletonInstance ?? (singletonInstance = new DataCache()); }
}
public string OnScreenMessage { get; set; }
public void AddStringToMessage(string c)
{
if (string.IsNullOrWhiteSpace(c)) return;
OnScreenMessage += c;
RaiseOnMessageChanged();
}
public void ClearMessage()
{
OnScreenMessage = string.Empty;
RaiseOnMessageChanged();
}
private void RaiseOnMessageChanged()
{
if (OnMessageChanged != null)
OnMessageChanged(null, null);
}
}
public class MainViewModel : ViewModelBase
{
private readonly MessageViewModel messageVM;
private readonly KeyboardViewModel keyboardVM;
private readonly ButtonsViewModel buttonsVM;
private readonly DataCache dataCache;
public MainViewModel()
{
messageVM = new MessageViewModel();
keyboardVM = new KeyboardViewModel();
buttonsVM = new ButtonsViewModel();
}
public ViewModelBase MessageViewModel { get { return messageVM; } }
public ViewModelBase KeyboardViewModel { get { return keyboardVM; } }
public ViewModelBase ButtonsViewModel { get { return buttonsVM; } }
}
public class MessageViewModel : ViewModelBase
{
private readonly DataCache dataCache = DataCache.Instance;
public MessageViewModel()
{
dataCache.OnMessageChanged += RaiseMessageChanged;
}
private void RaiseMessageChanged(object sender, EventArgs e)
{
OnPropertyChanged("Message");
}
public string Message
{
get { return dataCache.OnScreenMessage; }
set { dataCache.OnScreenMessage = value; }
}
}
public class KeyboardViewModel : ViewModelBase
{
private readonly DataCache dataCache = DataCache.Instance;
private ICommand onClickButtonCommand;
public ICommand OnClickButton
{
get
{
return onClickButtonCommand ?? (onClickButtonCommand = new RelayCommand(p => dataCache.AddStringToMessage((string)p)));
}
}
}
public class ButtonsViewModel : ViewModelBase
{
private readonly DataCache dataCache = DataCache.Instance;
private ICommand onGoBackCommand;
public ICommand OnGoBackButton
{
get
{
return onGoBackCommand ?? (onGoBackCommand = new RelayCommand(p => dataCache.ClearMessage()));
}
}
}
public class RelayCommand : ICommand
{
#region Fields
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
#endregion Fields
#region Constructors
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null) handler(this, EventArgs.Empty);
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion ICommand Members
}
<Window x:Class="StudentScoreWpfProj.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StudentScoreWpfProj"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MainViewModel,IsDesignTimeCreatable=True}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<local:MessgaeView DataContext="{Binding MessageViewModel}" />
<local:KeyboardView Grid.Row="1" DataContext="{Binding KeyboardViewModel}" />
<local:ButtonsView Grid.Row="2" DataContext="{Binding ButtonsViewModel}" />
</Grid>
<UserControl x:Class="StudentScoreWpfProj.ButtonsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StudentScoreWpfProj"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:ButtonsViewModel,IsDesignTimeCreatable=True}"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Horizontal">
<Button Content="GoBack" Command="{Binding OnGoBackButton}"></Button>
<Button Content="Continue"></Button>
</StackPanel>
</Grid>
<UserControl x:Class="StudentScoreWpfProj.KeyboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StudentScoreWpfProj"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:KeyboardViewModel,IsDesignTimeCreatable=True}"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel Orientation="Horizontal">
<Button Content="A" Command="{Binding OnClickButton}" CommandParameter="A"></Button>
<Button Content="B" Command="{Binding OnClickButton}" CommandParameter="B"></Button>
<Button Content="C" Command="{Binding OnClickButton}" CommandParameter="C"></Button>
</StackPanel>
</Grid>
<UserControl x:Class="StudentScoreWpfProj.MessgaeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StudentScoreWpfProj"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MessageViewModel,IsDesignTimeCreatable=True}"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBox Text="{Binding Message}"/>
</Grid>
You could do several things ...
You could create a static instance for easy access, and expose what you want on it (not recommended, read comments).
You could use dependency injection, so your other viewmodel will take the keyboard viewmodel as a parameter (please have a look at my other answer, it'll get you started quicly).
You could use a messenger to help you talk between them as well. most mvvm frameworks will have some ( have a look at this SO question, and at this code project article to get you started. They are specifically for MVVM light, but they'll help you understand the concept) .
How about using ServiceLocator from Microsoft.Practices.ServiceLocation?
ServiceLocator.Current.GetInstance<ViewModelName>();
I am setting the DataContext for my View in the View's Constructor to an instance of my ViewModel, just standard stuff. Shortly thereafter, an UPDATE_RECENT_DOCUMENTS_LIST Event fires from the Event Aggregator which my ViewModel catches correctly. A property is changed and the onPropertyChanged method is called, but it fails as the PropertyChanged event is null.
The very next thing I do is an action to the UI which raises a CREATE_PROJECT Event and the same ViewModel is receiving events, except now, the PropertyChanged event is no longer null and everything works as expected.
Is there a specific amount of time that has to pass after setting the DataContext before it registers to the PropertyChanged Event? Is there an event I can wait for that ensures the PropertyChanged event is not null?
Also, I did not run into this problem using standard .NET events, just after integrating Prism and using the very convenient EventAggregator.
I am showing my code behind of the View and the ViewModel, omitting the View XAML for brevity.
ToolBarView.xaml.cs:
namespace ToolBarModule
{
public partial class ToolBarView : UserControl
{
public ToolBarView(ToolBarViewModel toolBarViewModel)
{
InitializeComponent();
this.DataContext = toolBarViewModel;
}
}
}
ToolBarViewModel.cs
namespace ToolBarModule
{
public class ToolBarViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ToolBarCommands baseCommands;
private IEventAggregator eventAggregator;
private KickStartEvent kickStartEvent;
private SubscriptionToken subscriptionToken;
private ObservableCollection<IDocumentReference> recentDocuments = new ObservableCollection<IDocumentReference>();
private ActionCommand newTest;
private ActionCommand openTest;
private ActionCommand saveTest;
private ActionCommand exitApplication;
public ToolBarViewModel(){}
public ToolBarViewModel(IEventAggregator eventAggregator)
{
this.eventAggregator = eventAggregator;
baseCommands = new ToolBarCommands(eventAggregator);
kickStartEvent = eventAggregator.GetEvent<KickStartEvent>();
subscriptionToken = kickStartEvent.Subscribe(kickStartEventHandler, ThreadOption.UIThread, true, toolBarEventHandlerFilter);
}
public ICommand NewTest
{
get
{
if (newTest == null)
{
newTest = new ActionCommand(baseCommands.NewTestAction);
}
return newTest;
}
}
public ICommand OpenTest
{
get
{
if (openTest == null)
{
openTest = new ActionCommand(baseCommands.OpenTestAction);
}
return openTest;
}
}
public ICommand SaveTest
{
get
{
if (saveTest == null)
{
saveTest = new ActionCommand(baseCommands.SaveTestAction);
}
return saveTest;
}
}
public ICommand ExitApplication
{
get
{
if (exitApplication == null)
{
exitApplication = new ActionCommand(baseCommands.ExitApplicationAction);
}
return exitApplication;
}
}
public ObservableCollection<IDocumentReference> RecentDocuments
{
get
{
return recentDocuments;
}
set
{
recentDocuments = value;
onPropertyChanged("RecentDocuments");
}
}
private void onPropertyChanged(string propertyChanged)
{
if (PropertyChanged != null)
{
PropertyChanged(this,new PropertyChangedEventArgs(propertyChanged));
}
}
private void kickStartEventHandler(KickStartEventsArgs e)
{
switch (e.EventType)
{
case KickStartEventsArgs.KickStartEventType.CREATE_PROJECT:
onPropertyChanged("RecentDocuments");
break;
case KickStartEventsArgs.KickStartEventType.UPDATE_RECENT_DOCUMENTS_LIST:
RecentDocuments.Clear();
foreach (IDocumentReference recentDocs in e.KickStartTestList)
{
RecentDocuments.Add(recentDocs);
}
onPropertyChanged("RecentDocuments");
break;
}
}
}
}
You can also try to set the DataContext of a Grid or an Element below the UserControl. For me it worked.
Example (Doesn't work if you use DependencyProperty):
Code Behind:
public MyUserControl()
{
InitializeComponent();
this.DataContext = new { LabelText = "Hello World!" };
}
XAML
<UserControl x:Class="CoolProject.ViewModel.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Label x:Name="myLabel" Content="{Binding LabelText}"/>
Example 2 (My working code):
Code Behind:
public MyUserControl()
{
InitializeComponent();
this.myGrid.DataContext = new { LabelText = "Hello World!" };
}
XAML
<UserControl x:Class="CoolProject.ViewModel.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="myGrid">
<Label x:Name="myLabel" Content="{Binding LabelText}"/>
</Grid>
You have to name your UserControl in XAML and use it in binding. Something like following code:
<UserControl x:Name="uc" >
.
.
.
<TextBox Text="{Binding UserName, Mode=TwoWay, ElementName=uc}"/>
Where uc is a name of your UserControl, and Also try to set DataContext when UserControl loaded.
Hope this help.