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;
Related
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.
My idea is to have a window with 2 content controls changing to different views. For example, my app may start with a welcome image in one content control and enter/exit button in other. On enter button click, I want to change the welcome view to some other view and the enter/exit buttons to 3 other buttons.
My current problem is, trying to change the welcome view by clicking the enter button does not seem to work if I put it in a user control and host it in content control. My guess is because I am actually creating a new instance of my MainViewModel instead of referencing the existing one, but I am not entirely sure.
How can I make it so, when I click on my button in BottomPanelTwoButtonsView my HomeView changes to TestView?
There are 4 views in total:
HomeView - the welcome screen
TestView - some other view
BottomPanelTwoButtonsView - enter/exit buttons
BottomPanelThreeButtonsView - some 3 other buttons
Here's the basic code I have:
App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow app = new MainWindow();
MainViewModel context = new MainViewModel();
app.DataContext = context;
app.Show();
}
}
MainWindow.xaml
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:localViewModels="clr-namespace:MyApp.ViewModels"
xmlns:localViews="clr-namespace:MyApp.Views"
mc:Ignorable="d"
Name="RootWindow"
WindowStyle="None" ResizeMode="NoResize"
Topmost="True"
WindowStartupLocation="CenterScreen"
WindowState="Maximized"
Background="{StaticResource BackgroundSolidColorBrush}"
ToolBarTray.IsLocked="True"
NumberSubstitution.Substitution="European"
Title="MyApp" Height="1920" Width="1080">
<Window.Resources>
<DataTemplate DataType="{x:Type localViewModels:HomeViewModel}">
<localViews:HomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type localViewModels:TestViewModel}">
<localViews:TestView />
</DataTemplate>
<DataTemplate DataType="{x:Type localViewModels:BottomPanelTwoButtonsViewModel}">
<localViews:BottomPanelTwoButtons />
</DataTemplate>
<DataTemplate DataType="{x:Type localViewModels:BottomPanelThreeButtonsViewModel}">
<localViews:BottomPanelThreeButtons />
</DataTemplate>
</Window.Resources>
<DockPanel Background="White">
<ContentControl Content="{Binding CurrentBottomPanelViewModel}" DockPanel.Dock="Bottom" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Height="96" Width="1080">
</ContentControl>
<ContentControl Content="{Binding CurrentPageViewModel}" HorizontalAlignment="Stretch" VerticalAlignment="Top" HorizontalContentAlignment="Stretch" Height="1824" Width="1080">
</ContentControl>
</DockPanel>
</Window>
MainViewModel.cs
public class MainViewModel : ObservableObject
{
#region Fields
private ICommand _changePageCommand;
private IPageViewModel _currentPageViewModel;
private IBottomPanelViewModel _currentBottomPanelViewModel;
private ObservableCollection<IPageViewModel> _pageViewModels;
#endregion
public MainViewModel()
{
CurrentPageViewModel = new HomeViewModel();
CurrentBottomPanelViewModel = new BottomPanelTwoButtonsViewModel();
//CurrentBottomPanelViewModel = new BottomPanelThreeButtonsViewModel();
PageViewModels.Add(new ScreeningsScheduleViewModel());
PageViewModels.Add(new TestViewModel());
PageViewModels.Add(CurrentPageViewModel);
}
#region Properties / Commands
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
{
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p),
p => p is IPageViewModel);
}
return _changePageCommand;
}
}
public ObservableCollection<IPageViewModel> PageViewModels
{
get
{
if (_pageViewModels == null)
{
_pageViewModels = new ObservableCollection<IPageViewModel>();
}
return _pageViewModels;
}
set
{
_pageViewModels = value;
OnPropertyChanged("PageViewModels");
}
}
public IPageViewModel CurrentPageViewModel
{
get => _currentPageViewModel;
set
{
if (_currentPageViewModel != value)
{
_currentPageViewModel = value;
OnPropertyChanged("CurrentPageViewModel");
}
}
}
public IBottomPanelViewModel CurrentBottomPanelViewModel
{
get => _currentBottomPanelViewModel;
set
{
if (_currentBottomPanelViewModel != value)
{
_currentBottomPanelViewModel = value;
OnPropertyChanged("CurrentBottomPanelViewModel");
}
}
}
#endregion
#region Methods
private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
{
PageViewModels.Add(viewModel);
}
CurrentPageViewModel = PageViewModels
.FirstOrDefault(vm => vm == viewModel);
}
#endregion
}
BottomPanelTwoButtonsView.xaml
<UserControl x:Class="MyApp.Views.BottomPanelTwoButtons"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mvm="clr-namespace:MyApp"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="1080">
<UserControl.DataContext>
<mvm:MainViewModel/>
</UserControl.DataContext>
<Grid Height="96" Background="White" Width="1080">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Column="0"
Content="1"
Command="{Binding DataContext.ChangePageCommand, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type mvm:MainViewModel}}, Mode=TwoWay}"
CommandParameter="{Binding PageViewModels[2]}"
IsEnabled="True"
Style="{StaticResource RoundCornerButtonCyanFilledBig}"
/>
<Button Grid.Column="1" Style="{StaticResource RoundCornerButtonMagentaFilledBig}"/>
</Grid>
</UserControl>
IPageViewModel.cs
public interface IPageViewModel
{
string Name { get; }
}
ObservableObject.cs
public abstract class ObservableObject : INotifyPropertyChanged
{
[Conditional("DEBUG")]
[DebuggerStepThrough]
public virtual void VerifyPropertyName(string propertyName)
{
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (!ThrowOnInvalidPropertyName)
{
Debug.Fail(msg);
}
else
{
throw new Exception(msg);
}
}
}
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
public virtual void RaisePropertyChanged(string propertyName)
{
VerifyPropertyName(propertyName);
OnPropertyChanged(propertyName);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
RelayCommand.cs
public class RelayCommand : ICommand
{
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute ?? throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
[DebuggerStepThrough]
public bool CanExecute(object parameters)
{
return _canExecute == null ? true : _canExecute(parameters);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameters)
{
_execute(parameters);
}
}
I guess that your problem is within the BottomPanelTwoButtonsView.xaml
There you have the following lines:
<UserControl.DataContext>
<mvm:MainViewModel/>
</UserControl.DataContext>
So you're overwriting your datacontext which is defined in the Window.Resources of the MainWindow
I have set a bool property and have bound it to the IsEnabled in the xaml but the ICommand CanExecute method overrides the IsEnabled in xaml, so my bool property is ineffective.
When I define the conditions within the CanExecute method in the view model, It either disables all buttons in which the method is bound to, or enables all of them.
Its a grid that displays 3 different buttons for each row, and each button goes to a new xaml screen. If there is no data for the particular condition on the row the button is on then the button needs to be disabled.
How do i go about setting this so that buttons are disabled upon a condition?
Custom Command:
public class CustomCommand : ICommand
{
private Action<object> execute;
private Predicate<object> canExecute;
public CustomCommand(Action<object> execute, Predicate<object> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
}
remove
{
}
}
public bool CanExecute(object parameter)
{
//throw new NotImplementedException();
bool b = canExecute == null ? true : canExecute(parameter);
return b;
}
public void Execute(object parameter)
{
execute(parameter);
}
}
xaml
<DataTemplate>
<Button Command="{Binding Source={StaticResource VM},
Path=Command}" CommandParameter="{Binding}" >
<SymbolIcon Symbol="Edit" Foreground="AliceBlue" />
</Button>
</DataTemplate>
CanExecute in VM
private bool CanGetDetails(object obj)
{
return true;
}
You can always do your conditional statement within the CanExecute function of your custom command, no need for you to bind IsEnabled property with your button that is bound to a command. Here's a sample implementation, hope this helps.
Custom Command:
public class CustomCommand<T> : ICommand
{
private readonly Action<T> _action;
private readonly Predicate<T> _canExecute;
public CustomCommand(Action<T> action, Predicate<T> canExecute)
{
_action = action;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute((T)parameter);
}
public void Execute(object parameter)
{
_action((T)parameter);
}
public event EventHandler CanExecuteChanged;
}
As you can see here, I created an object that implements the ICommand interface, this custom command accepts a generic type parameter which is used to evaluate a condition (CanExecute: this tells whether to enable or disable a command (in UI, the button), normally use to check for permissions, and other certain conditions) this parameter is also used to execute the action (Execute: the actual logic/action to be performed), The command contructor accepts delegate parameters that contain signatures for these 2 methods, the caller may choose lambda or standard methods to fillup these parameters.
Sample ViewModel:
public class ViewModel1: INotifyPropertyChanged
{
public ViewModel1()
{
// Test Data.
Items = new ObservableCollection<ItemViewModel>
{
new ItemViewModel{ Code = "001", Description = "Paint" },
new ItemViewModel{ Code = "002", Description = "Brush" },
new ItemViewModel{ Code = "003", Description = "" }
};
EditCommand = new CustomCommand<ItemViewModel>(Edit, CanEdit);
}
public CustomCommand<ItemViewModel> EditCommand { get; }
private bool CanEdit(ItemViewModel item)
{
return item?.Description != string.Empty;
}
private void Edit(ItemViewModel item)
{
Debug.WriteLine("Selected Item: {0} - {1}", item.Code, item.Description);
}
private ObservableCollection<ItemViewModel> _items { get; set; }
public ObservableCollection<ItemViewModel> Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Page x:Name="root"
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vms="using:App1.ViewModels"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DesignHeight="450" d:DesignWidth="800">
<Page.DataContext>
<vms:ViewModel1 x:Name="Model"/>
</Page.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0 0 0 15">
<TextBlock Text="{Binding Code}" />
<TextBlock Text="{Binding Description}" />
<Button Content="Edit" Command="{Binding DataContext.EditCommand, ElementName=root}" CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Page>
I think you can pick a lot of code from the RelayCommand of MVVMLight. Try to change your event to
public event EventHandler CanExecuteChanged
{
add
{
if (canExecute != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (canExecute != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
and add also a function
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
Then, whatever you put as your Predicate on the command, at the Predicate's boolean setter do:
SomeCustomCommand.RaiseCanExecuteChanged()
Hope I helped.
My task: I want to bind textbox and button.
Although I found many topics about it I cannot manage my problem.
I have project: Client with WPF application WITH DEFAULT XAML no BINDING, which takes context from MenuWindow project, which is library. Inside MenuWindow project I have User Control WPF called: MenuProgram.
<UserControl x:Class="MenuWindow.MenuProgram"
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:MenuWindow"
mc:Ignorable="d"
d:DesignHeight="550" d:DesignWidth="780">
<UserControl.DataContext>
<local:MenuViewModel/>
</UserControl.DataContext>
<Grid Background="#FF6F6FA4">
<Label x:Name="lblTitle" Content="GUI Export Revit Data" HorizontalAlignment="Left" Margin="277,31,0,0" VerticalAlignment="Top" Height="50" Width="258" FontSize="24" FontWeight="Bold"/>
<Label x:Name="lblPrtdPath" Content="File prtd path" HorizontalAlignment="Left" Margin="200,176,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblXmlPath1" Content="File xml path1" HorizontalAlignment="Left" Margin="200,222,0,0" VerticalAlignment="Top"/>
<Label x:Name="lblXmlPath2" Content="File xml path2" HorizontalAlignment="Left" Margin="200,266,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="tbxPrtd" HorizontalAlignment="Left" Height="23" Margin="302,176,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="268" Text="{Binding PrtdFilePath}"/>
<TextBox x:Name="tbxXml1" HorizontalAlignment="Left" Height="23" Margin="302,222,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="268" Text="{Binding XmlFilePath1}"/>
<TextBox x:Name="tbxXml2" HorizontalAlignment="Left" Height="23" Margin="302,266,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="268" Text="{Binding XmlFilePath2}"/>
<Button x:Name="SayHi" Content="Start" HorizontalAlignment="Left" Margin="302,450,0,0" VerticalAlignment="Top" Width="174" Height="84" FontSize="22" Command="{Binding SayHi}" />
<Button x:Name="btnAbout" Content="About" HorizontalAlignment="Left" Margin="705,496,0,0" VerticalAlignment="Top" Width="55" Height="38" Command="{Binding SayHi}"/>
</Grid>
so I have
<UserControl.DataContext>
<mv:MenuViewModel/>
</UserControl.DataContext>
and with textBoxs or button I want to use binding.
in codeBehind this User Control there is nothing but default initialization.
In Project Menu there are:
MenuArguments.cs with mapping:
public string PrtdFilePath { get; set; }
public string XmlFilePath1 { get; set; }
public string XmlFilePath2 { get; set; }
RelayCommand:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MenuWindow
{
public class RelayCommand : ICommand
{
private readonly Func<Boolean> _canExecute;
private readonly Action _execute;
public RelayCommand(Action execute)
: this(execute, null)
{
}
public RelayCommand(Action execute, Func<Boolean> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
public Boolean CanExecute(Object parameter)
{
return _canExecute == null ? true : _canExecute();
}
public void Execute(Object parameter)
{
_execute();
}
}
}
and MenuViewModel.cs
namespace MenuWindow
{
public class MenuViewModel : INotifyPropertyChanged
{
public string gowno;
public MenuArguments _menuArgumenty;
public string PrtdFilePath
{
get { return _menuArgumenty.PrtdFilePath; }
set
{
_menuArgumenty.PrtdFilePath = value;
OnPropertyChanged("PrtdFilePath");
}
}
public string XmlFilePath1
{
get { return _menuArgumenty.XmlFilePath1; }
set
{
_menuArgumenty.XmlFilePath1 = value;
OnPropertyChanged("XmlFilePath1");
}
}
public string XmlFilePath2
{
get { return _menuArgumenty.XmlFilePath2; }
set
{
_menuArgumenty.XmlFilePath2 = value;
OnPropertyChanged("XmlFilePath2");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public MenuViewModel()
{
_menuArgumenty = new MenuArguments();
}
public ICommand SayHi
{
get
{
return new RelayCommand(SayHiExcute, CanSayHiExcute);
}
}
private void SayHiExcute()
{
if (!MenuArgumentsExists(_menuArgumenty))
{
MessageBox.Show(string.Format("Hi {0} {1}!", _menuArgumenty.PrtdFilePath, _menuArgumenty.XmlFilePath1));
SavePerosn(_menuArgumenty);
}
else
{
MessageBox.Show(string.Format("Hey {0} {1}, you exists in our database!", _menuArgumenty.PrtdFilePath, _menuArgumenty.XmlFilePath1));
}
}
private void SavePerosn(MenuArguments _menuArgumenty)
{
//Some Database Logic
}
private bool CanSayHiExcute()
{
return !MenuArgumentsExists(_menuArgumenty);
}
private bool MenuArgumentsExists(MenuArguments _menuArgumenty)
{
//Some logic
return false;
}
}
}
When I start program debuger goes through binding properties. After window appears there is no reaction from binding. What do I do wrong? Please help me.
BR,
student Cenarius
Thanks for comments, answers to your comments:
#tabby - I want to bind textBoxes: PrtdFilePath, XmlFilePath1, XmlFilePath1 and button SayHi
#maulik kansara - You are right, I was trying some another methods and I didnt remove code. It should be only version with local.
#grek40 - My example works for one-project in solution for Window not for UserControl which is set in another project. Here is picture:
#mm8 - I expected by puting data ino textBoxes or clicking button to see breakpoint in:
public string PrtdFilePath
{
get { return _menuArgumenty.PrtdFilePath; }
set
{
_menuArgumenty.PrtdFilePath = value;
OnPropertyChanged("PrtdFilePath");
}
}
Finally, I think that code in XAML is problem. I was reading about parent-child relations with finding binding/viewmodel/path but I am confused and I dont know how to solve it. Please help me thanks You for all comments.
#grek40 here is Code in Main APP WPF, I add context from my MenuWindow.
This MainWindow WPF APP has default XAML.
public MainWindow()
{
InitializeComponent();
menuProgram = new MenuProgram();//User Control
sw = new SharedWindow();//WPF window
this.Close();
sw.Content = menuProgram.Content;// here I set context
sw.ShowDialog();
}
and XAML:
<Window x:Class="Client.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:Client"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
</Grid>
</Window>
Whole code with Your change:
public partial class MainWindow : Window
{
private SharedWindow sw;
private MenuProgram menuProgram;
public MainWindow()
{
InitializeComponent();
menuProgram = new MenuProgram();
SetForContext();
}
private void SetForContext()
{
sw = new SharedWindow();
this.Close();
sw.Content = menuProgram;
sw.ShowDialog();
}
You need to set the UserControl as window Content, not the Content of UserControl:
sw.Content = menuProgram;// here I set context
/* Bad: sw.Content = menuProgram.Content; */
Your DataContext is assigned to the UserControl itself, so if you move the Content tree to a different Parent, it will no longer have its old DataContext.
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>();