In my VM, I have an event
public class ViewModelBase
{
public delegate bool MyDel(object param);
public MyDel MyEvent;
public void TransferClick()
{
MyEvent(null); // to simulate the click at View
}
}
And in the View, currently I have the following code ( behind):
public class View: UserControl
{
private void UserControl1_Load(Object sender, EventArgs e)
{
(DataContext as ViewModelBase).MyEvent+=SimulateClick;
}
private bool SimulateClick(object param)
{
//some logic to simulate clicks on the View, on the user control
}
}
So that the VM can invoke the SimulateClick logic in View whenever it has to.
I don't like this approach because it pollutes my view's code behind. Any way to make the MyEvent bind to XAML instead, much like how I bind VM ICommand to existing button clicks and stuff like that?
Note: I don't actually want to simulate mouse clicks ( I know I can use ICommand to do just that), just want to do some events like mouse clicks event on my MVVM model.
If you view model needs to tell the view to do something you could use an event aggregator or a messenger to send a message from the view model to the view in a loosely coupled way:
https://blog.magnusmontin.net/2014/02/28/using-the-event-aggregator-pattern-to-communicate-between-view-models/
https://msdn.microsoft.com/en-us/magazine/jj694937.aspx
The benefit of using this pattern is that the view model and the view don't need to know anything about each other.
The other option would be to inject the view model with an interface type that the view implements:
public interface IView
{
bool SimulateClick(object param);
}
public partial class View : UserControl, IView
{
public View()
{
InitializeComponent();
DataContext = new ViewModel(this);
}
public bool SimulateClick(object param)
{
//...
}
}
This doesn't really break the MVVM pattern as the view model only has a dependency upon an interface that the view happens to implement.
Updated answer
First of all - I would highly recommend the approach #mm8 has suggested, or exposing Command(s) (such as RefreshCommand) on your views to achieve the same.
But if that is not an option; then I believe you can create a custom attached event that can technically bind the view-model's event to the control's eventhandler; while maintaining the MVVM level of separation.
For example, you can define an attached event in following manner:
// ViewModel event args
public class MyEventArgs : EventArgs
{
public object Param { get; set; }
}
// Interim args to hold params during event transfer
public class InvokeEventArgs : RoutedEventArgs
{
public InvokeEventArgs(RoutedEvent e) : base(e) { }
public object Param { get; set; }
}
// Base view model
public class ViewModelBase
{
public event EventHandler<MyEventArgs> MyEvent1;
public event EventHandler<MyEventArgs> MyEvent2;
public void TransferClick1()
{
MyEvent1?.Invoke(this, new MyEventArgs { Param = DateTime.Now }); // to simulate the click at View
}
public void TransferClick2()
{
MyEvent2?.Invoke(this, new MyEventArgs { Param = DateTime.Today.DayOfWeek }); // to simulate the click at View
}
}
// the attached behavior that does the magic binding
public class EventMapper : DependencyObject
{
public static string GetTrackEventName(DependencyObject obj)
{
return (string)obj.GetValue(TrackEventNameProperty);
}
public static void SetTrackEventName(DependencyObject obj, string value)
{
obj.SetValue(TrackEventNameProperty, value);
}
public static readonly DependencyProperty TrackEventNameProperty =
DependencyProperty.RegisterAttached("TrackEventName",
typeof(string), typeof(EventMapper), new PropertyMetadata
(null, new PropertyChangedCallback(OnTrackEventNameChanged)));
private static void OnTrackEventNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
FrameworkElement uie = d as FrameworkElement;
if (uie == null)
return;
var eventName = GetTrackEventName(uie);
if (string.IsNullOrWhiteSpace(eventName))
return;
EventHandler<MyEventArgs> vmEventTracker = delegate (object sender, MyEventArgs e) {
Application.Current.Dispatcher.Invoke(() =>
uie.RaiseEvent(new InvokeEventArgs(EventMapper.OnInvokeEvent)
{
Source = sender,
Param = e?.Param
}));
};
uie.DataContextChanged += (object sender, DependencyPropertyChangedEventArgs e) =>
{
var oldVM = e.OldValue;
var newVM = e.NewValue;
if (oldVM != null)
{
var eventInfo = oldVM.GetType().GetEvent(eventName);
eventInfo?.RemoveEventHandler(oldVM, vmEventTracker);
}
if (newVM != null)
{
var eventInfo = newVM.GetType().GetEvent(eventName);
eventInfo?.AddEventHandler(newVM, vmEventTracker);
}
};
var viewModel = uie.DataContext;
if (viewModel != null)
{
var eventInfo = viewModel.GetType().GetEvent(eventName);
eventInfo?.AddEventHandler(viewModel, vmEventTracker);
}
}
public static readonly RoutedEvent OnInvokeEvent =
EventManager.RegisterRoutedEvent("OnInvoke",
RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(EventMapper));
public static void AddOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
{
FrameworkElement uie = d as FrameworkElement;
if (uie != null)
{
uie.AddHandler(OnInvokeEvent, handler);
}
}
public static void RemoveOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
{
FrameworkElement uie = d as FrameworkElement;
if (uie != null)
{
uie.RemoveHandler(OnInvokeEvent, handler);
}
}
}
Sample 1 - Event handler
XAML Usage
<StackPanel Margin="20">
<Button Margin="10" Content="Invoke VM event" Click="InvokeEventOnVM" />
<Button Content="View Listener1"
local:EventMapper.TrackEventName="MyEvent1"
local:EventMapper.OnInvoke="SimulateClick1" />
<Button Content="View Listener2"
local:EventMapper.TrackEventName="MyEvent1"
local:EventMapper.OnInvoke="SimulateClick1" />
<Button Content="View Listener3"
local:EventMapper.TrackEventName="MyEvent2"
local:EventMapper.OnInvoke="SimulateClick2" />
</StackPanel>
Sample code-Behind for above XAML:
private void SimulateClick1(object sender, RoutedEventArgs e)
{
(sender as Button).Content = new TextBlock { Text = (e as InvokeEventArgs)?.Param?.ToString() };
}
private void SimulateClick2(object sender, RoutedEventArgs e)
{
SimulateClick1(sender, e);
(sender as Button).IsEnabled = !(sender as Button).IsEnabled; //toggle button
}
private void InvokeEventOnVM(object sender, RoutedEventArgs e)
{
var vm = new ViewModelBase();
this.DataContext = vm;
vm.TransferClick1();
vm.TransferClick2();
}
Sample 2 - Event Trigger (updated 07/26)
XAML Usage
<Button Content="View Listener"
local:EventMapper.TrackEventName="MyEvent2">
<Button.Triggers>
<EventTrigger RoutedEvent="local:EventMapper.OnInvoke">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
The VM will receive a command from other VMs ( think of FileVM
notifies tabVM), and then the VM (tabVM) will load the data at the
view via the SimulateClick method at View (tabView).
Why the hell would you name method SimulateClick if the method loads data based on some command? I would refactor your code like this.
public delegate bool MyDel(object data);
public class ViewModelBase
{
public MyDel SomeCommandExecuted;
void SomeCommand_Execute()
{
string[] sampleData = new [] //this may come from the other viewmodel for example
{
"Item1", "Item2", "Items3";
}
MyDel handler = SomeCommandExecuted;
if (handler != null)
{
handler(sampleData);
}
}
}
if you exposed the functionality like this, it is ok to attach to the event in codebehind. Why would you pollute XAML with attaching to the VM's event and calling codebehind method? It's better to attach to the event in codebehind, because at least your code remains type safe and refactorable.
public class View: UserControl
{
public View()
{
this.InitializeComponent();
Loaded += (o, e) => ViewModel.SomeCommandExecuted += ViewModel_SomeCommandExecuted;
}
ViewModelBase ViewModel => (ViewModelBase)DataContext;
private bool ViewModel_SomeCommandExecuted(object data)
{
//load the data into view
}
}
Attaching to the VM's event in codebehind is not violation of MVVM. However, the resposibility of ViewModel is to expose data in such form that is easily consumable from View (usually via databinding).
here is my suggestion:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool InDesignMode => DesignerProperties.GetIsInDesignMode(new DependecyObject());
}
public class ViewModel : ViewModelBase
{
private string[] _data;
//ctor
public ViewModel()
{
if (IsInDesignMode)
{
Data = new [] { "Visible", "In", "XAML", "Designer" }
}
}
public string[] Data
{
get { return _data; }
set { _data = value; OnPropertyChanged(); }
}
void SomeCommand_Execute()
{
string[] sampleData = new [] //this may come from the other viewmodel for example
{
"Item1", "Item2", "Items3";
}
Data = sampleData;
}
}
I have prepared the data in ViewModel so that they are easily consumable in view. Once they are ready, I notify view using PropertyChanged event. Now I can easily bind ItemsControl, ListView, etc in View. No codebehind needed what's so ever. This is the purpose of ViewModel
You can use x:bind for function binding. That way your xaml can bind and directly invoke the view models event handler without needing a "pass through" invoke method in the view.
Click="{x:Bind viewModel.Foo}"
More docs
Here is an example of event binding in wpf
WPF event binding from View to ViewModel?
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<StackPanel Background="Transparent">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap">
<command:EventToCommand
Command="{Binding Main.NavigateToArticleCommand,
Mode=OneWay,
Source={StaticResource Locator}}"
CommandParameter="{Binding Mode=OneWay}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</StackPanel>
Viewmodel
public RelayCommand NavigateToArticleCommand
{
get
{
return _navigateToArticleCommand
?? (_navigateToArticleCommand= new RelayCommand(
async () =>
{
await SomeCommand();
}));
}
}
Related
I'm trying to implement the example at:
https://github.com/olohmann/WpfRxControls
There are three parts to the custom control:
PART_TextBox
PART_Popup
PART_ListBox
Relevant source:
https://github.com/olohmann/WpfRxControls/blob/master/WpfRxControls/AutoCompleteTextBox.cs
https://github.com/olohmann/WpfRxControls/blob/master/WpfRxControls/Themes/Generic.xaml
All the pieces are in place, and the code using the new control is as follows:
<ctrls:AutoCompleteTextBox
Grid.Row="1"
AutoCompleteQueryResultProvider="{Binding AutoCompleteQueryResultProvider}"
Margin="10" FontSize="20" PopupHeight="300">
</ctrls:AutoCompleteTextBox>
I just need to hook into the ListBox's SelectionChanged event in my pages XAML / ViewModel, how can this be accomplished?
Edit: In XAML / VM, not view code behind. Thus far all view code behinds are empty and I'd like to keep it that way.
I thought there was some way to override PART_ListBox in a ControlTemplate override in MainWindow.XAML?
Edit: Final solution, thanks to mm8
In the AutoCompleteTextBox.cs, create a dependency property of type ICommand:
public const string AutoCompleteSelectionChangedPropertyName = "AutoCompleteSelectionChangedCommand";
public ICommand AutoCompleteSelectionChangedCommand
{
get { return (ICommand) GetValue(AutoCompleteSelectionChangedProperty); }
set { SetValue(AutoCompleteSelectionChangedProperty, value);}
}
public static readonly DependencyProperty AutoCompleteSelectionChangedProperty = DependencyProperty.Register(
AutoCompleteSelectionChangedPropertyName,
typeof(ICommand),
typeof(AutoCompleteTextBox));
In the SetResultText method:
AutoCompleteSelectionChangedCommand?.Execute(autoCompleteQueryResult);
View / ViewModel usage:
<ac:AutoCompleteTextBox Name="AutoComplete"
AutoCompleteQueryResultProvider="{Binding AutoCompleteQueryResultProvider}"
FontSize="12"
AutoCompleteSelectionChangedCommand="{Binding CommandEditValueChanged}">
</ac:AutoCompleteTextBox>
public ICommand CommandEditValueChanged { get; set; }
public MainWindowViewModel(){
CommandEditValueChanged = new DelegateCommand<object>(OnEditValueChanged);
}
private void OnEditValueChanged(object result){
// do stuff
}
You could handle the Loaded event of the AutoCompleteTextBox in the view, get a reference to the PART_ListBox in the control template using the FindName method and then hook up an event handler for the SelectionChanged event of the ListBox:
<ctrls:AutoCompleteTextBox
Grid.Row="1"
AutoCompleteQueryResultProvider="{Binding AutoCompleteQueryResultProvider}"
Margin="10" FontSize="20" PopupHeight="300" Loaded="AutoCompleteTextBox_Loaded">
</ctrls:AutoCompleteTextBox>
private void AutoCompleteTextBox_Loaded(object sender, RoutedEventArgs e)
{
AutoCompleteTextBox actb = sender as AutoCompleteTextBox;
ListBox lb = actb.Template.FindName("PART_ListBox", actb) as ListBox;
if (lb != null)
{
lb.SelectionChanged += (ss, ee) =>
{
MainWindowViewModel vm = DataContext as MainWindowViewModel;
//invoke a command of the view model or do whatever you want here...
var selectedItem = lb.SelectedItem;
};
}
}
Your view model class has no (and shouldn't have any) reference nor knowledge about the ListBox that is part of the template of the control.
I thought there was some way to override PART_ListBox in a ControlTemplate override in MainWindow.XAML?
Then you will have to override/re-define the entire ControlTemplate of the AutoCompleteTextBox control which seems a bit unnecessary.
MVVM is not about eliminating code from the views - it's about separation of concerns and whether you hook up an event handler from the XAML markup of the view or the code-behind of the very same view makes no difference at all as far as the design pattern is concerned.
Edit: But if you want to keep the code-behind classes clean you could implement this using an attached behaviour:
public class AutoCompleteBoxBehavior
{
public static ICommand GetSelectionChangedCommand(AutoCompleteTextBox actb)
{
return (ICommand)actb.GetValue(SelectionChangedCommandProperty);
}
public static void SetSelectionChangedCommand(AutoCompleteTextBox actb, ICommand value)
{
actb.SetValue(SelectionChangedCommandProperty, value);
}
public static readonly DependencyProperty SelectionChangedCommandProperty =
DependencyProperty.RegisterAttached(
"SelectionChangedCommand",
typeof(ICommand),
typeof(AutoCompleteBoxBehavior),
new UIPropertyMetadata(null, OnHandleSelectionChangedEvent));
private static void OnHandleSelectionChangedEvent(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ICommand command = e.NewValue as ICommand;
if(command != null)
{
AutoCompleteTextBox actb = d as AutoCompleteTextBox;
actb.Loaded += (ss, ee) =>
{
ListBox lb = actb.Template.FindName("PART_ListBox", actb) as ListBox;
if (lb != null)
{
lb.SelectionChanged += (sss, eee) =>
{
command.Execute(null);
};
}
};
}
}
}
<ctrls:AutoCompleteTextBox
Grid.Row="1"
AutoCompleteQueryResultProvider="{Binding AutoCompleteQueryResultProvider}"
Margin="10" FontSize="20" PopupHeight="300"
local:AutoCompleteBoxBehavior.SelectionChangedCommand="{Binding YourCommand}">
</ctrls:AutoCompleteTextBox>
Introduction to Attached Behaviors in WPF: https://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF
I have simplified app to show my issue
When I click button, it changes Text property of ViewModel and TextBlock.Text is updated.
MainPage.xaml
<StackPanel>
<Button Click="ButtonBase_OnClick">Button to change text</Button>
<TextBlock Text="{x:Bind ViewModel.Text, Mode=OneWay}"></TextBlock>
</StackPanel>
MainPage.xaml.cs
public MainPage()
{
ViewModel = new ViewModel();
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Text = "x:Bind works";
}
ViewModel class has one string property (Text) and implemented INotifyPropertyChange interface.
Problem starts when ViewModel is not set in ctor (i.e. viewModel is null and changed in runtime):
public MainPage()
{
//ViewModel = new ViewModel();//this line has been removed
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();//this line has been added
ViewModel.Text = "x:Bind does not work";
}
Complited binding is not working (Text is not changed) and I could not figure out why it is so... I need to change viewModel from null (vm is null because it is waiting for some data in real app)
{x:Bind} bindings (often referred-to as compiled bindings) uses generated code to achieve its benefits. At XAML load time, {x:Bind} is converted into what you can think of as a binding object, and this object gets a value from a property on a data source. These generated code can be found in your obj folder, with names like (for C#) <view name>.g.cs.
For your code, the generated code will like following:
// Update methods for each path node used in binding steps.
private void Update_(global::UWP.BlankPage3 obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel(obj.ViewModel, phase);
}
}
}
private void Update_ViewModel(global::UWP.ViewModel obj, int phase)
{
this.bindingsTracking.UpdateChildListeners_ViewModel(obj);
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel_Text(obj.Text, phase);
}
}
}
...
private global::UWP.ViewModel cache_ViewModel = null;
public void UpdateChildListeners_ViewModel(global::UWP.ViewModel obj)
{
if (obj != cache_ViewModel)
{
if (cache_ViewModel != null)
{
((global::System.ComponentModel.INotifyPropertyChanged)cache_ViewModel).PropertyChanged -= PropertyChanged_ViewModel;
cache_ViewModel = null;
}
if (obj != null)
{
cache_ViewModel = obj;
((global::System.ComponentModel.INotifyPropertyChanged)obj).PropertyChanged += PropertyChanged_ViewModel;
}
}
}
Here I just copy some method that related to your issue. From these method, you can find that before update TextBlock or PropertyChanged listeners, it will check if the ViewModel is null. If it is null, nothing will be done. So to make {x:Bind} work, we must initialize ViewModel before page loaded. And this is the reason why {x:Bind} doesn't work when you initialize ViewModel in Button.Click event.
To fix this issue, you can implement INotifyPropertyChanged interface for ViewModel like Filip said so that the generated code can be notified when ViewModel changed (from null to new ViewModel()) and update you UI.
But I think you can just initialize ViewModel in constructor. When you initialize ViewModel, you can set the properties that you are waiting for to null first like:
public MainPage()
{
ViewModel = new ViewModel() { Text = null };
this.InitializeComponent();
}
And then update these properties when your date is ready. In this way, you can do not implement INotifyPropertyChanged interface on your page.
Besides these, there is another cheaper way, you can call this.Bindings.Update(); method to force the bindings to be updated after you initialize ViewModel like following:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();
ViewModel.Text = "x:Bind does not work";
this.Bindings.Update();
}
Did you implement INotifyPropertyChanged on page like so
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private ViewModel viewModel;
public ViewModel ViewModel
{
get { return viewModel; }
set
{
viewModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModel)));
}
}
public MainPage()
{
ViewModel = new ViewModel { };
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel { };//this line has been added
ViewModel.Text = "x:Bind does not work";
}
public event PropertyChangedEventHandler PropertyChanged;
}
This works for me.
I've a hard time understanding why ICommand.CanExecutes always contains the previous value instead of the new value if a nested property is used instead of a normal property.
The problem is described below and I seriously can't figure out a way to fix this besides using some form of "Facade" pattern where I create properties in the viewmodel and hook them to their corresponding property in the model.
Or use the damn CommandManager.RequerySuggested event. The reason this is not optimal is because the view presents over 30 commands, just counting the menu, and if all CanExecute updates every time something changes, it will take a few seconds for all menuitems / buttons to update. Even using the example down below with only a single command and button together with the command manager it takes around 500ms for the button to enable/disable itself.
The only reason I can think of is that the CommandParameter binding is not updated before the CanExecute is fired and then I guess there is nothing you can do about it.
Thanks in advance :!
For example
Let's say we've this basic viewmodel
public class BasicViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set {
this.name = value;
RaisePropertyChanged("Name");
Command.RaiseCanExecuteChanged();
}
}
private Project project;
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
if (value != null) value.PropertyChanged += ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
}
}
private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) {
Command.RaiseCanExecuteChanged();
}
public DelegateCommand<string> Command { get; set; }
public BasicViewModel()
{
this.Project = new Example.Project();
Command = new DelegateCommand<string>(this.Execute, this.CanExecute);
}
private bool CanExecute(string arg) {
return !string.IsNullOrWhiteSpace(arg);
}
private void Execute(string obj) { }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null) {
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
and this model
public class Project : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
RaisePropertyChanged("Text");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Now in my view I've this textbox and button.
<Button Content="Button" CommandParameter="{Binding Path=Project.Text}" Command="{Binding Path=Command}" />
<TextBox Text="{Binding Path=Project.Text, UpdateSourceTrigger=PropertyChanged}" />
It works, every time I type something in the textbox the CanExecute is invoked, BUT the parameter is always set to the previous value. Let say I write 'H' in the textbox, CanExecute is fired with parameter set to NULL. Next I write 'E', now the textbox contains "HE" and the CanExecute fires again. This time with the parameter set to 'H' only.
For some strange reason the parameter is always set to the previous value and when I check the Project.Text it's set to "HE" but parameter is still set to only 'H'.
If I now change the command parameter to
CommandParameter="{Binding Path=Name}"
and the Textbox.Text to
Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"
everything works perfectly. The CanExecute parameter always contain the latest value and not the previous value.
The facade pattern you're talking about it standard WPF practice. The main problem with the way that you're doing it is that when events are raised, their subscribed event handlers execute in the order that they are subscribed. The line of code where you have:
if (value != null) value.PropertyChanged += ChildPropertyChanged;
This subscribes to the "PropertyChanged" Event of your "Project" class. Your UIElements are also subscribed to this same "PropertyChanged" event through your binding in the XAML. In short, your "PropertyChanged" event now has 2 subscribers.
The thing about events is that they fire in a sequence and what's happening in your code, is that when the event fires from your "Project.Text" it executes your "ChildPropertyChanged" event, firing your "CanExecuteChanged" event, which finally runs your "CanExecute" function(which is when you're seeing the incorrect parameter).
THEN, after that, your UIElements get their EventHandlers executed by that same event. And their values get updated.
It's the order of your subscriptions causing the problem. Try this and tell me if it fixes your problem:
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
if (project != null) project.PropertyChanged += ChildPropertyChanged;
}
}
This is how I would have done this, and it works as expected. The only difference here is I'm using RelayCommand instead of DelegateCommand - they fundamentally have the same implementation so they should be interchangeable.
When the user enters the text and then clicks the button the execute method of the RelayCommand gets the expected text - simple.
XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Grid.Column="0"
Grid.Row="0"
Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="0"
Grid.Row="1"
Content="Test"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Command="{Binding Path=TextCommand, Mode=OneWay}" />
</Grid>
ViewModel:
public sealed class ExampleViewModel : BaseViewModel
{
private string _text;
public ExampleViewModel()
{
TextCommand = new RelayCommand(TextExecute, CanTextExecute);
}
public string Text
{
get
{
return _text;
}
set
{
_text = value;
OnPropertyChanged("Text");
}
}
public ICommand TextCommand { get; private set; }
private void TextExecute()
{
// Do something with _text value...
}
private bool CanTextExecute()
{
return true;
}
}
I found this great attached property from swythan on the prism codeplex discussion forum that did the trick very well. Of course it does not answer why the command parameter is set to the previous value but it fixes the problem in a nice way.
The code is slightly modified from the source, enabling the possibility to use it on controls in a TabItem by calling HookCommandParameterChanged when the OnLoaded event is invoked.
public static class CommandParameterBehavior
{
public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
typeof(bool),
typeof(CommandParameterBehavior),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));
public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
{
return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
}
public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
{
target.SetValue(IsCommandRequeriedOnChangeProperty, value);
}
private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ICommandSource))
return;
if (!(d is FrameworkElement || d is FrameworkContentElement))
return;
if ((bool)e.NewValue)
HookCommandParameterChanged(d);
else
UnhookCommandParameterChanged(d);
UpdateCommandState(d);
}
private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
{
return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
}
private static void HookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);
// N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
// so we need to hook the Unloaded event and call RemoveValueChanged there.
HookUnloaded(source);
}
private static void UnhookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);
UnhookUnloaded(source);
}
private static void HookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded += OnUnloaded;
fe.Loaded -= OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded += OnUnloaded;
fce.Loaded -= OnLoaded;
}
}
private static void UnhookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded -= OnUnloaded;
fe.Loaded += OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded -= OnUnloaded;
fce.Loaded += OnLoaded;
}
}
static void OnLoaded(object sender, RoutedEventArgs e)
{
HookCommandParameterChanged(sender);
}
static void OnUnloaded(object sender, RoutedEventArgs e)
{
UnhookCommandParameterChanged(sender);
}
static void OnCommandParameterChanged(object sender, EventArgs ea)
{
UpdateCommandState(sender);
}
private static void UpdateCommandState(object target)
{
var commandSource = target as ICommandSource;
if (commandSource == null)
return;
var rc = commandSource.Command as RoutedCommand;
if (rc != null)
CommandManager.InvalidateRequerySuggested();
var dc = commandSource.Command as IDelegateCommand;
if (dc != null)
dc.RaiseCanExecuteChanged();
}
}
Source: https://compositewpf.codeplex.com/discussions/47338
How could I access a XAML object in my ViewModel? I am really confused. I want to access the <Controls:ModalContentPresenter> object. How could I realise this in a MVVM conform way? On this object I want to call a method ShowModalContent
<Controls:ModalContentPresenter x:Name="modalContent">
<ScrollViewer Behaviors:AdvancedZooming.KeepInCenter="true" Visibility="{Binding LeerformularIsVisible}" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Viewbox Stretch="Uniform">
<Grid>
<DataGrid BorderBrush="{x:Null}">
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding AddFieldDefinitionCommand}" Header="Feld hinterlegen" Icon="pack://application:,,,/Images/Designer/field.png" />
<MenuItem Command="{Binding AddFunctionCommand}" Header="Funktion hinterlegen" Icon="pack://application:,,,/Images/Designer/FI_Taschenmesser_16x16.png" />
<MenuItem Command="{Binding RemoveFieldDefinitionCommand}" Header="Aktuelle Felddefinition entfernen" Icon="pack://application:,,,/Images/Designer/remove_field.png" />
<MenuItem Command="{Binding CutCommand}" Header="Ausschneiden" Icon="pack://application:,,,/Images/Zwischenablage/FI_Ausschneiden_16x16.png" />
<MenuItem Command="{Binding CopyCommand}" Header="Kopieren" Icon="pack://application:,,,/Images/Zwischenablage/FI_Kopieren_16x16.png" />
<MenuItem Command="{Binding PasteCommand}" Header="Einfügen" Icon="pack://application:,,,/Images/Zwischenablage/FI_Einfuegen_16x16.png" />
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
</Grid>
</Viewbox>
</ScrollViewer>
<Controls:ModalContentPresenter.ModalContent>
<StackPanel>
<TextBlock>Test</TextBlock>
<Button>Hide</Button>
</StackPanel>
</Controls:ModalContentPresenter.ModalContent>
</Controls:ModalContentPresenter>
The code of ModalContentPresenter can be found here:
using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
namespace Controls
{
[ContentProperty("Content")]
public class ModalContentPresenter : FrameworkElement
{
#region private fields
private Panel layoutRoot;
private ContentPresenter primaryContentPresenter;
private ContentPresenter modalContentPresenter;
private Border overlay;
private object[] logicalChildren;
private KeyboardNavigationMode cachedKeyboardNavigationMode;
private static readonly TraversalRequest traversalDirection;
#endregion
#region dependency properties
public static readonly DependencyProperty IsModalProperty = DependencyProperty.Register("IsModal", typeof(bool), typeof(ModalContentPresenter),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsModalChanged));
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(ModalContentPresenter),
new UIPropertyMetadata(null, OnContentChanged));
public static readonly DependencyProperty ModalContentProperty = DependencyProperty.Register("ModalContent", typeof(object), typeof(ModalContentPresenter),
new UIPropertyMetadata(null, OnModalContentChanged));
public static readonly DependencyProperty OverlayBrushProperty = DependencyProperty.Register("OverlayBrush", typeof(Brush), typeof(ModalContentPresenter),
new UIPropertyMetadata(new SolidColorBrush(Color.FromArgb(204, 169, 169, 169)), OnOverlayBrushChanged));
public bool IsModal
{
get { return (bool)GetValue(IsModalProperty); }
set { SetValue(IsModalProperty, value); }
}
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public object ModalContent
{
get { return (object)GetValue(ModalContentProperty); }
set { SetValue(ModalContentProperty, value); }
}
public Brush OverlayBrush
{
get { return (Brush)GetValue(OverlayBrushProperty); }
set { SetValue(OverlayBrushProperty, value); }
}
#endregion
#region routed events
public static readonly RoutedEvent PreviewModalContentShownEvent = EventManager.RegisterRoutedEvent("PreviewModalContentShown", RoutingStrategy.Tunnel,
typeof(RoutedEventArgs), typeof(ModalContentPresenter));
public static readonly RoutedEvent ModalContentShownEvent = EventManager.RegisterRoutedEvent("ModalContentShown", RoutingStrategy.Bubble,
typeof(RoutedEventArgs), typeof(ModalContentPresenter));
public static readonly RoutedEvent PreviewModalContentHiddenEvent = EventManager.RegisterRoutedEvent("PreviewModalContentHidden", RoutingStrategy.Tunnel,
typeof(RoutedEventArgs), typeof(ModalContentPresenter));
public static readonly RoutedEvent ModalContentHiddenEvent = EventManager.RegisterRoutedEvent("ModalContentHidden", RoutingStrategy.Bubble,
typeof(RoutedEventArgs), typeof(ModalContentPresenter));
public event RoutedEventHandler PreviewModalContentShown
{
add { AddHandler(PreviewModalContentShownEvent, value); }
remove { RemoveHandler(PreviewModalContentShownEvent, value); }
}
public event RoutedEventHandler ModalContentShown
{
add { AddHandler(ModalContentShownEvent, value); }
remove { RemoveHandler(ModalContentShownEvent, value); }
}
public event RoutedEventHandler PreviewModalContentHidden
{
add { AddHandler(PreviewModalContentHiddenEvent, value); }
remove { RemoveHandler(PreviewModalContentHiddenEvent, value); }
}
public event RoutedEventHandler ModalContentHidden
{
add { AddHandler(ModalContentHiddenEvent, value); }
remove { RemoveHandler(ModalContentHiddenEvent, value); }
}
#endregion
#region ModalContentPresenter implementation
static ModalContentPresenter()
{
traversalDirection = new TraversalRequest(FocusNavigationDirection.First);
}
public ModalContentPresenter()
{
layoutRoot = new ModalContentPresenterPanel();
primaryContentPresenter = new ContentPresenter();
modalContentPresenter = new ContentPresenter();
overlay = new Border();
AddVisualChild(layoutRoot);
logicalChildren = new object[2];
overlay.Background = OverlayBrush;
overlay.Child = modalContentPresenter;
overlay.Visibility = Visibility.Hidden;
layoutRoot.Children.Add(primaryContentPresenter);
layoutRoot.Children.Add(overlay);
}
public void ShowModalContent()
{
if (!IsModal)
IsModal = true;
}
public void HideModalContent()
{
if (IsModal)
IsModal = false;
}
private void RaiseModalContentShownEvents()
{
RoutedEventArgs args = new RoutedEventArgs(PreviewModalContentShownEvent);
OnPreviewModalContentShown(args);
if (!args.Handled)
{
args = new RoutedEventArgs(ModalContentShownEvent);
OnModalContentShown(args);
}
}
private void RaiseModalContentHiddenEvents()
{
RoutedEventArgs args = new RoutedEventArgs(PreviewModalContentHiddenEvent);
OnPreviewModalContentHidden(args);
if (!args.Handled)
{
args = new RoutedEventArgs(ModalContentHiddenEvent);
OnModalContentHidden(args);
}
}
protected virtual void OnPreviewModalContentShown(RoutedEventArgs e)
{
RaiseEvent(e);
}
protected virtual void OnModalContentShown(RoutedEventArgs e)
{
RaiseEvent(e);
}
protected virtual void OnPreviewModalContentHidden(RoutedEventArgs e)
{
RaiseEvent(e);
}
protected virtual void OnModalContentHidden(RoutedEventArgs e)
{
RaiseEvent(e);
}
#endregion
#region property changed callbacks
private static void OnIsModalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ModalContentPresenter control = (ModalContentPresenter)d;
if ((bool)e.NewValue == true)
{
control.cachedKeyboardNavigationMode = KeyboardNavigation.GetTabNavigation(control.primaryContentPresenter);
KeyboardNavigation.SetTabNavigation(control.primaryContentPresenter, KeyboardNavigationMode.None);
control.overlay.Visibility = Visibility.Visible;
control.overlay.MoveFocus(traversalDirection);
control.RaiseModalContentShownEvents();
}
else
{
control.overlay.Visibility = Visibility.Hidden;
KeyboardNavigation.SetTabNavigation(control.primaryContentPresenter, control.cachedKeyboardNavigationMode);
control.primaryContentPresenter.MoveFocus(traversalDirection);
control.RaiseModalContentHiddenEvents();
}
}
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ModalContentPresenter control = (ModalContentPresenter)d;
if (e.OldValue != null)
control.RemoveLogicalChild(e.OldValue);
control.primaryContentPresenter.Content = e.NewValue;
control.AddLogicalChild(e.NewValue);
control.logicalChildren[0] = e.NewValue;
}
private static void OnModalContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ModalContentPresenter control = (ModalContentPresenter)d;
if (e.OldValue != null)
control.RemoveLogicalChild(e.OldValue);
control.modalContentPresenter.Content = e.NewValue;
control.AddLogicalChild(e.NewValue);
control.logicalChildren[1] = e.NewValue;
}
private static void OnOverlayBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ModalContentPresenter control = (ModalContentPresenter)d;
control.overlay.Background = (Brush)e.NewValue;
}
#endregion
#region FrameworkElement overrides
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index > 1)
throw new ArgumentOutOfRangeException("index");
return layoutRoot;
}
protected override int VisualChildrenCount
{
get { return 1; }
}
protected override IEnumerator LogicalChildren
{
get { return logicalChildren.GetEnumerator(); }
}
protected override Size ArrangeOverride(Size finalSize)
{
layoutRoot.Arrange(new Rect(finalSize));
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
layoutRoot.Measure(availableSize);
return layoutRoot.DesiredSize;
}
#endregion
#region layout panel
class ModalContentPresenterPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
Size resultSize = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Measure(availableSize);
resultSize.Width = Math.Max(resultSize.Width, child.DesiredSize.Width);
resultSize.Height = Math.Max(resultSize.Height, child.DesiredSize.Height);
}
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in InternalChildren)
{
child.Arrange(new Rect(finalSize));
}
return finalSize;
}
}
#endregion
}
}
You shouldn't do this (at least, this is definitely not a MVVM-way).
Use IsModal property and data binding instead:
<Controls:ModalContentPresenter x:Name="modalContent" IsModal="{Binding IsModal}">
where IsModal in the binding expression is a bound data property within your view model.
In MVVM, any data going back and forth should be done via data binding properties on your View Model. That includes virtually any properties of the ContentPresenter - just add a binding. However, if you want call a method of a XAML object in an MVVM-friendly way, you can use an Action.
Let's suppose I wanted to set the focus on a textbox via the view model (contrived example, I know there's other MVVM-ways to do this- just wanted an example that requires a child object of the view).
First, give the textbox that should get the focus a name in your XAML, i.e.,
<TextBox x:Name="textBox1"/>
On your view model, add a property for the Action:
public Action FocusAction {get;set;}
Some time before or as your view is loading, get your DataContext (i.e., your view model) and add the Action to the code behind (the view's .cs file):
ViewModel vm = (ViewModel)this.DataContext;
if ( vm.FocusAction == null )
vm.FocusAction= new Action(() => this.textBox1.Focus());
For instance, you might implement the IsLoaded event and do it there. You could also do this in your constructor after InitializeComponent, as long as it either created your view model instance or it was passed in as a paramater. The key is that the view model is already instantiated and assigned to the view's data context.
Then in your view model, wherever you wanted to add focus to that textbox, call:
FocusAction();
Since what I just showed above requires that your view cast the DataContext to a particular view model, what I do is create an interface for the kinds of actions I need, like this:
interface IFocusable
{
Action FocusAction {get;set;}
}
Then I make my view model implement that inteface. With that done, in my view's code-behind, I can say something like:
if(this.DataContext is IFocusable)
((IFocusable)this.DataContext).FocusAction = new Action(() => this.textBox1.Focus());
I think that makes it more MVVM-compliant, since it's not tightly-coupled to a particular view model, the view just knows it can add an action if the view model is the type of view model that can use it.
More details and another example available here: http://jkshay.com/closing-a-wpf-window-using-mvvm-and-minimal-
code-behind/
I know it's been few years but I just faced the same, so here's my answer in case someone will find it useful...
In your xaml file refer to your ViewModel class in the DataContext:
<Window.DataContext>
<local:YourViewModel x:Name="yourViewModel"/>
</Window.DataContext>
In your ViewModel class create a public function with your xaml file as argument and a private member to hold it:
private MainWindow mainWindow;
public void OnViewInitialized(MainWindow mainWindow)
{
this.mainWindow = mainWindow;
}
In your code behind use the function pass the the 'yourViewModel' you defined in the xaml:
public MainWindow()
{
InitializeComponent();
yourViewModel.OnViewInitialized(this);
}
And that's it :)
Now you can access all your xaml elements from your ViewModel by using your mainWindow member, just give them a name.
mainWindow.textBox1
you can create a constructor for view model which accepts the ContentPage as its parameter.
public class ViewModelClass
{
public ViewModelClass(ContentPage p=null)
{...}
}
then set the binding context in Contentpage back code script passing the referenc of contentpage to viewmodel
public class ContentPageClass : ContentPage
{
public ContentPageClass()
{
BindingContext = new ViewModelClass(p:this);
}
}
Is there a MVVM way to select text in a textbox? The MVVM framework that I am using is Laurent Bugnion's MVVM Light Toolkit.
Whenever I am trying to directly affect the the View in a "pure" MVVM application (no code-behind in View), I will use Attached Properties to encapsulate whatever effect I am trying to achieve. I will create an interface that defines the actions I wish to take using custom events. I then implement this interface in each ViewModel that will be "running" these commands on the View. Finally, I bind my ViewModel to the attached property in my View definition. The following code shows how to this for SelectAll and a TextBox. This code can be easily expanded to perform just about any action on any component in the View.
My Attached Property and interface definition:
using System.Windows;
using System.Windows.Controls;
using System;
using System.Collections.Generic;
namespace SelectAllSample
{
public static class TextBoxAttach
{
public static readonly DependencyProperty TextBoxControllerProperty = DependencyProperty.RegisterAttached(
"TextBoxController", typeof(ITextBoxController), typeof(TextBoxAttach),
new FrameworkPropertyMetadata(null, OnTextBoxControllerChanged));
public static void SetTextBoxController(UIElement element, ITextBoxController value)
{
element.SetValue(TextBoxControllerProperty, value);
}
public static ITextBoxController GetTextBoxController(UIElement element)
{
return (ITextBoxController)element.GetValue(TextBoxControllerProperty);
}
private static readonly Dictionary<ITextBoxController, TextBox> elements = new Dictionary<ITextBoxController, TextBox>();
private static void OnTextBoxControllerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as TextBox;
if (element == null)
throw new ArgumentNullException("d");
var oldController = e.OldValue as ITextBoxController;
if (oldController != null)
{
elements.Remove(oldController);
oldController.SelectAll -= SelectAll;
}
var newController = e.NewValue as ITextBoxController;
if (newController != null)
{
elements.Add(newController, element);
newController.SelectAll += SelectAll;
}
}
private static void SelectAll(ITextBoxController sender)
{
TextBox element;
if (!elements.TryGetValue(sender, out element))
throw new ArgumentException("sender");
element.Focus();
element.SelectAll();
}
}
public interface ITextBoxController
{
event SelectAllEventHandler SelectAll;
}
public delegate void SelectAllEventHandler(ITextBoxController sender);
}
My ViewModel definition:
public class MyViewModel : ITextBoxController
{
public MyViewModel()
{
Value = "My Text";
SelectAllCommand = new RelayCommand(p =>
{
if (SelectAll != null)
SelectAll(this);
});
}
public string Value { get; set; }
public RelayCommand SelectAllCommand { get; private set; }
public event SelectAllEventHandler SelectAll;
}
My View definition:
<Window x:Class="SelectAllSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SelectAllSample"
Title="Window1" Height="150" Width="150">
<x:Code><![CDATA[
public Window1()
{
InitializeComponent();
DataContext = new MyViewModel();
}
]]></x:Code>
<StackPanel>
<TextBox Text="{Binding Value}" loc:TextBoxAttach.TextBoxController="{Binding}" />
<Button Content="Select All" Command="{Binding SelectAllCommand}" />
</StackPanel>
</Window>
Note: Thanks to Josh Smith for RelayCommand (see code in Figure 3 on this page). It is used in MyViewModel in this example (and just about all my MVVM code).
find a good introduction to attached properties here:
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx