Avalonia: Binding Command Property to UserControl - c#

So I created a custom button control, let's call it MyButton, using Avalonia. MyButton is a collection of several controls including a Avalonia.Controls.Button looking like this (MyButton.xaml):
<Border xmlns="https://github.com/avaloniaui"
.....
x:Class="myProject.myControls.MyButton">
<Button x:Name="button"
Background="Transparent"
....
BorderThickness="0">
<Panel >
.....
</Panel>
</Button>
</Border>
(Yes my custom control inherits from Avalonia.Controls.Border instead of Avalonia.Controls.UserControl)
My plan is to pass the buttons Command property (the one with the x:Name="button" attribute) further up and make it accessable via MyButton.
So when I want to use MyButton in the MainWindow.xaml I'd to be able to do the following:
<Window ... >
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<myControls:MyButton Command="{Binding MyButton_Click}"/>
</Window>
where the view model MainWindowViewModel.cs looks like this:
public partial class MainWindowViewModel : ViewModelBase
{
public void MyButton_Click()
{
// do stuff...
}
}
The way I tried to do this in MyButton.xaml.cs is the following:
public class MyButton : Border
{
private readonly Button button;
public MyButton()
{
InitializeComponent();
button = this.FindControl<Button>("button");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static readonly StyledProperty<ICommand> CommandProperty =
AvaloniaProperty.Register<MyButton, ICommand>(nameof(Command));
public ICommand Command
{
get { return GetValue(CommandProperty); }
set
{ // this setter is never executed as can be seen when running with debugger attached
if (button != null)
{
button.Command = value;
SetValue(CommandProperty, value);
}
else
{
Debug.WriteLine("MyButton error: unable to set Command: control not initialized!");
}
}
}
}
However when running the application and clicking the button the target method MyButton_Click is never executed. Attaching the debugger it seems like the MyButton.Command setter is never executed either, which I think would be due to incorrect Binding? (There are no binding errors or anything related to this on the debug console)
After a few hours of trial and error I found a workaround using Reflection and a custom OnClick() Eventhandler on the button element. It works but is kinda ugly and requires a static target method so my question is:
How does one properly bind a Command on a UserControl to a method contained in the ViewModel of the main Window?
Also: Could my reflection-based approach also be viable? (I assume Avalonia bindings are also based on reflection somehow?)

Do not use getters and setters for styled properties, they won't be called when property is altered via bindings, styles or animations (it's the same for WPF, UWP and Xamarin.Forms). Instead you need to either bind your nested Button's command via <Button Command="{Binding $parent[myControls:MyButton]}" /> (preferable) or subscribe to property change notification from the static constructor like the original Button does.
More on dependency properties (which work mostly the same way as StyledProperty in Avalonia): https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/dependency-properties-overview

Related

WPF mvvm View won't update when called from different ViewModel

I've been trying to update a property inside a usercontrol with it's own VM.
<Label Content="{Binding Path =LabelText ,UpdateSourceTrigger=PropertyChanged }" Grid.Column="0"/>
The Label itself works when I set it's text in the VM's constructor, and also updates if I use a command within the VM.
But changing it from outside won't do anything:
private LabelTextBoxVM _testThing;
public LabelTextBoxVM TestThing
{
get => _testThing;
set
{
_testThing = value;
NotifyPropertyChanged("TestThing");
}
}
private void Update()
{
TestThing.LabelText = "Eureka";
TestThing.TextBoxText = "Wooosh";
NotifyPropertyChanged("TestThing");
}
From all I've read, calling NotifyPropertyChanged for TestThing should update the content of both Label and Textbox, but nothing happens.
#Clemens
I found the answer, provided by you actually, in another question:
wpf-mvvm-usercontrol-binding
I have to set my DataContext in the parent view, binding to a property in the parent, and delete the Datacontext inside the childVM
So:
<myViewes:UserControlName DataContext="{Binding VMProperty}/>"
And this part cannot be in my UserControls:
<UserControl.DataContext>
<viewModels:UserControlVM/>
</UserControl.DataContext>
I am guessing this was what you meant by "inheriting" the VM
Thank you!

How to run a method every time a TabItem is selected, in an MVVM application using Prism

I have been trying to implement this for a while and haven't been able to do it so far, despite having the feeling that this should be something easy.
The difficulty comes from the fact that I have implemented a WPF application using the MVVM pattern. Now, this is my first attempt at both the pattern and the framework, so it is almost guaranteed that I have made mistakes while trying to follow the MVVM guidelines.
My implementation
I have three Views with their respective ViewModels (wired using Prism's AutoWireViewModel method). The MainView has a TabControl with two TabItems, each of witch contains a Frame container with the Source set to one of the other two Views. The following code is an excerpt of the MainView:
<TabControl Grid.Row="1" Grid.Column="1">
<TabItem Header="Test">
<!--TestView-->
<Frame Source="View1.xaml"/>
</TabItem>
<TabItem Header="Results">
<!--ResultsView-->
<Frame Source="View2.xaml"/>
</TabItem>
</TabControl>
My problem
Every time that someone changes to a specific TabItem, I would like to run a method that updates one of the WPF controls included in that View. The method is already implemented and bound to a Button, but ideally, no button should be necessary, I would like to have some kind of Event to make this happen.
I appreciate all the help in advance.
You could for example handle the Loaded event of the Page to either call a method or invoke a command of the view model once the view has been loaded initially:
public partial class View2 : Page
{
public View2()
{
InitializeComponent();
Loaded += View2_Loaded;
}
private void View2_Loaded(object sender, RoutedEventArgs e)
{
var viewModel = DataContext as ViewModel2;
if (viewModel != null)
viewModel.YourCommand.Execute(null);
Loaded -= View2_Loaded;
}
}
The other option would be handle this in the MainViewModel. You bind the SelectedItem property of the TabControl to a property of the MainViewModel and set this property to an instance of either ViewModel2 or ViewModel2, depending on what kind of view you want to display.
You could then call any method or invoked any command you want on these. But this is another story and then you shouldn't hardcode the TabItems in the view and use Frame elements to display Pages. Please take a look here for an example:
Selecting TabItem in TabControl from ViewModel
Okay, so What I have done is Create a Custom Tab Control. I will write out step by step instructions for this, and then you can add edit to it.
Right click on your solution select add new project
Search For Custom Control Library
High Light the name of the class that comes up, and right click rename it to what ever you want I named it MyTabControl.
Add Prism.Wpf to the new project
Add a reference to the new project to where ever your going to need it. I needed to add to just the main application, but if you have a separate project that only has views then you will need to add it to that too.
Inherit your Custom Control From TabControl Like:
public class MyTabControl : TabControl
You will notice that there is a Themes folder in the project you will need to open the Generic.xaml and edit it. it should look like:
TargetType="{x:Type local:MyTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" for some reason this will not let me show the style tags but they will need to be in there as well
Please review this code I got this from Add A Command To Custom Control
public class MyTabControl : TabControl
{
static MyTabControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTabControl), new FrameworkPropertyMetadata(typeof(MyTabControl)));
}
public static readonly DependencyProperty TabChangedCommandProperty = DependencyProperty.Register(
"TabChangedCommand", typeof(ICommand), typeof(MyTabControl),
new PropertyMetadata((ICommand)null,
new PropertyChangedCallback(CommandCallBack)));
private static void CommandCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myTabControl = (MyTabControl)d;
myTabControl.HookupCommands((ICommand) e.OldValue, (ICommand) e.NewValue);
}
private void HookupCommands(ICommand oldValue, ICommand newValue)
{
if (oldValue != null)
{
RemoveCommand(oldValue, oldValue);
}
AddCommand(oldValue, oldValue);
}
private void AddCommand(ICommand oldValue, ICommand newCommand)
{
EventHandler handler = new EventHandler(CanExecuteChanged);
var canExecuteChangedHandler = handler;
if (newCommand != null)
{
newCommand.CanExecuteChanged += canExecuteChangedHandler;
}
}
private void CanExecuteChanged(object sender, EventArgs e)
{
if (this.TabChangedCommand != null)
{
if (TabChangedCommand.CanExecute(null))
{
this.IsEnabled = true;
}
else
{
this.IsEnabled = false;
}
}
}
private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
{
EventHandler handler = CanExecuteChanged;
oldCommand.CanExecuteChanged -= handler;
}
public ICommand TabChangedCommand
{
get { return (ICommand) GetValue(TabChangedCommandProperty); }
set { SetValue(TabChangedCommandProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (TabChangedCommand != null)
{
TabChangedCommand.Execute(null);
}
}
}
you will need to add the name space in your window or usercontrol like:
xmlns:wpfCustomControlLibrary1="clr-namespace:WpfCustomControlLibrary1;assembly=WpfCustomControlLibrary1"
and here is your control:
<wpfCustomControlLibrary1:MyTabControl TabChangedCommand="{Binding TabChangedCommand}">
<TabItem Header="View A"></TabItem>
<TabItem Header="View B"></TabItem>
</wpfCustomControlLibrary1:MyTabControl>
This is how I'd approach this sort of requirement:
View:
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="tc" ItemsSource="{Binding vms}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type local:uc1vm}">
<local:UserControl1/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:uc2vm}">
<local:UserControl2/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding TabHeading}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
When it has a uc1vm it will be templated into usercontrol1 in the view.
I'm binding to a collection of viewmodels which all implement an interface so I know for sure I can cast to that and call a method.
Main viewmodel for window:
private IDoSomething selectedVM;
public IDoSomething SelectedVM
{
get { return selectedVM; }
set
{
selectedVM = value;
selectedVM.doit();
RaisePropertyChanged();
}
}
public ObservableCollection<IDoSomething> vms { get; set; } = new ObservableCollection<IDoSomething>
{ new uc1vm(),
new uc2vm()
};
public MainWIndowViewModel()
{
}
When a tab is selected, the setter for selected item will be passed the new value. Cast that and call the method.
My interface is very simple, since this is just illustrative:
public interface IDoSomething
{
void doit();
}
An example viewmodel, which is again just illustrative and doesn't do much:
public class uc1vm : IDoSomething
{
public string TabHeading { get; set; } = "Uc1";
public void doit()
{
// Your code goes here
}
}
I appreciate all of your input, but I found an alternative solution. Given the information given by #mm8, I took advantage of the Loaded event but in a way that does not require any code in the code behind.
My solution
In the View which I would like to give this ability to execute a method every time the user selects the TabItem that contains it, I added the following code:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding OnLoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
And then simply implemented a DelegateCommand called OnLoadedCommand in the View's respective ViewModel. Inside that command I call my desired method.
Please comment if you spot anything wrong with this approach! I chose to try this since it required the least amount of changes to my code, but I may be missing some vital information regarding problems the solution may cause.

UserControl DataContext Binding

I have three projects in my solution:
My main WPF Application which contains a MainWindow + MainViewModel
UserControl Library with a UserControl (ConfigEditorView)
UIProcess class with the ViewModel for the UserControl (ConfigEditorViewModel)
In my MainWindow I want to use the UserControl with the ViewModel of UIProcess.
First I set the UserControl in my MainWindow:
<TabItem Header="Editor">
<Grid>
<cel:ConfigEditorView DataContext="{Binding ConfEditModel, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</TabItem>
I don't know which of these properties I need here, so I put all together but it still doesn't work.
Then I've set this in my MainViewModel:
public ConfigEditorViewModel ConfEditModel { get; set; }
With simple method that is bound to a Button:
private void doSomething()
{
ConfEditModel = new ConfigEditorViewModel("Hello World");
}
My ConfigEditorViewModel looks basically like this:
public class ConfigEditorViewModel : ViewModelBase
{
private string _Description;
public string Description
{
get
{
return _Description;
}
set
{
_Description = value;
base.RaisePropertyChanged();
}
}
public ConfigEditorViewModel(string t)
{
Description = t;
}
}
The description is bound to a TextBox in my UserControl.
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Description}"/>
When I start the application and click the Button the TextBox should contain "Hello World" but it's empty.
What I've done wrong?
i gave you a general answer:
within a "real(a usercontrol you wanna use with different viewmodels with different property names)" usercontrol you bind just to your own DependencyProperties and you do that with ElementName or RelativeSource binding and you should never set the DataContext within a UserControl.
<UserControl x:Name="myRealUC" x:class="MyUserControl">
<TextBox Text="{Binding ElementName=myRealUC, Path=MyOwnDPIDeclaredInMyUc, Path=TwoWay}"/>
<UserControl>
if you do that you can easily use this Usercontrol in any view like:
<myControls:MyUserControl MyOwnDPIDeclaredInMyUc="{Binding MyPropertyInMyViewmodel}"/>
and for completeness: the Dependency Property
public readonly static DependencyProperty MyOwnDPIDeclaredInMyUcProperty = DependencyProperty.Register(
"MyOwnDPIDeclaredInMyUc", typeof(string), typeof(MyUserControl), new PropertyMetadata(""));
public bool MyOwnDPIDeclaredInMyUc
{
get { return (string)GetValue(MyOwnDPIDeclaredInMyUcProperty); }
set { SetValue(MyOwnDPIDeclaredInMyUcProperty, value); }
}
Your view models (and, optionally, models) need to implement INotifyPropertyChanged.
Binding's aren't magic. There is no inbuilt mechanism that allows for code to be notified when a plain old property's value changes. You'd have to poll it in order to check to see if a change happened, which would be very bad, performance-wise.
So bindings will look at the objects they are bound against and see if they implement INotifyPropertyChanged and, if so, will subscribe to the PropertyChanged event. That way, when you change a property and fire the event, the binding is notified and updates the UI.
Be warned, you must implement the interface and use it correctly. This example says it's for 2010, but it works fine.

WPF :: Custom Control's Dependency Properties Aren't Working

I'm making a Ribbon control for a WYSIWYG HTML editor. The ribbon has the typical Bold, Italic, Underline, FontFamily, etc. controls that you'd expect to see. I'll focus on the Bold functionality for this example.
I want the Ribbon to be reuseable, so I've added a Dependency Property (DP) and associated property wrapper to the control's code behind (standard boilerplate stuff):
public partial class EditorRibbon: UserControl
{
public static readonly DependencyProperty IsBoldProperty =
DependencyProperty.Register(
"IsBold",
typeof (bool),
typeof (EditorRibbon),
new PropertyMetadata(default(bool)));
public bool IsBold
{
get { return (bool) GetValue(IsBoldProperty); }
set { SetValue(IsBoldProperty, value); }
}
}
... and in the XAML I have my RibbonToggleButton, and I've bound the IsChecked property to the dependency property:
<UserControl x:Class="My.EditorRibbon">
<r:RibbonToggleButton Command="ToggleBold"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
SmallImageSource="{StaticResource ToggleBoldIcon}"
IsChecked="{Binding IsBold}" />
</UserControl>
In my Editor window, I've bound the IsBold property of the EditorRibbon to a conventional property on the window's ViewModel:
<Window x:class="My.MainWindow>
<My.EditorRibbon IsBold="{Binding SelectionIsBold}"/>
</Window>
Here is the SelectionIsBold property:
public bool SelectionIsBold
{
get { return _selection.IsBold(); }
}
... and I raise the NotifyPropertyChanged() event (in the MainWindow's ViewModel) whenever the selection in the RichTextBox changes:
public class MainWindowViewModel : BaseViewModel
{
public MainWindowViewModel(MainWindow window)
{
rtb.SelectionChanged += rtb_OnSelectionChanged;
}
private void rtb_OnSelectionChanged(object sender, RoutedEventArgs routedEventArgs)
{
NotifyPropertyChanged(()=>SelectionIsBold);
}
}
To my mind, this should be enough to change the IsChecked state of the RibbonToggleButton whenever the selection changes... but it doesn't. Despite changing the selection, and despite the NotifyPropertyChanged() firing as expected, a breakpoint on the SelectionIsBold property (yes, I've deselected VS's "Step Over Property" setting) is never hit. Somewhere, the request to refresh the value isn't propagating correctly.
Do I need to trigger NotifyPropertyChanged() on the IsBold property after the value is set in the setter?
Change the IsBold binding to the following
<UserControl x:Class="My.EditorRibbon" x:Name="EditorRibbonInstance">
<r:RibbonToggleButton Command="ToggleBold"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
SmallImageSource="{StaticResource ToggleBoldIcon}"
IsChecked="{Binding IsBold, ElementName=EditorRibbonInstance, Mode=TwoWay}" />
</UserControl>
With that you are sure that the binding is going to the property of the control and not to the datacontext of the control
You have to fire notifypropertychanged in ViewModel. Try somethings like this in ViewModel:
protected void FirePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
->> FirePropertyChanged("SelectionIsBold")
The reason is: now, your data context is ViewModel, all of binding to ViewModel must be triggered by ViewModel's properties
first of all, I never saw the injection of the Window to the ViewModel before... are you using some Kind of DI for the injection?
I think it is not a good idea to use the selection changed Event on viewmodel... This is not mvvm from my Point of view...
Are you updating the _selection somewhere? Might be that you always checking the same selection?!
You are not properly binding the command property of your button.
Should reflect something like this:
Command="{Binding ToggleBold}"

link command of KeyBinding in mainwindow's view model to an user control's view model commandbindings

I have a mainwindow inside which there is a usercontrol which contains a listview.
User control also has a button which copies all the contents of listview to clipboard.
This is how the copy functionality has been implemented.
below is a part of xaml of usercontrol -
<Button Command="Copy"
CommandTarget="{Binding ElementName=testCodeView}"
CommandParameter="Copy"
</Button>
<ListView x:Name="testCodeView"
ItemsSource="{Binding Products}" BorderThickness="0" Grid.Row="1"
ItemTemplate="{StaticResource testViewTemplate}"
ItemContainerStyle="{StaticResource testCodesListItem}"
infra:AttachedProperties.CommandBindings ="{Binding CommandBindings}">
</ListView>
The AttachedProperties class holds the Dependency property "CommandBindings" - below is the code -
public class AttachedProperties
{
public static DependencyProperty CommandBindingsProperty =
DependencyProperty.RegisterAttached("CommandBindings", typeof(CommandBindingCollection), typeof(AttachedProperties),
new PropertyMetadata(null, OnCommandBindingsChanged));
public static void SetCommandBindings(UIElement element, CommandBindingCollection value)
{
if (element != null)
element.SetValue(CommandBindingsProperty, value);
}
public static CommandBindingCollection GetCommandBindings(UIElement element)
{
return (element != null ? (CommandBindingCollection)element.GetValue (CommandBindingsProperty) : null);
}
}
Below is the usercontrol viewmodel's code related to copying the items of listview.
public class UserControlViewModel : INotifyPropertyChanged
{
public CommandBindingCollection CommandBindings
{
get
{
if (commandBindings_ == null)
{
commandBindings_ = new CommandBindingCollection();
}
return commandBindings_;
}
}
public UserControlViewModel
{
CommandBinding copyBinding = new CommandBinding(ApplicationCommands.Copy,
this.CtrlCCopyCmdExecuted, this.CtrlCCopyCmdCanExecute);
// Register binding to class
CommandManager.RegisterClassCommandBinding(typeof(UserControlViewModel), copyBinding);
this.CommandBindings.Add(copyBinding);
}
private void CtrlCCopyCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
copyToclipboard_.CtrlCCopyCmdExecuted(sender, e);
}
}
The sender object in CtrlCCopyCmdExecuted function is the listview in usercontrol which is futher used in
copying its contents.
The copy all functionality works fine with the button on user control.
I have to create a key binding for copy functionality in mainwindow. I have created other keybindings in mainwindow which works fine as the commands are defined in MainWindowViewModel, but since the commandbindings of the copy all command are in the usercontrol's view model, I am facing problems in linking command of keybinding in mainwindowviewmodel with the commandbinding in usercontrolviewmodel.
Can Someone help me out with this.
Thanks in advance.
If your parent view model has access to the child view model then you have two main options. Both options involve creating an ICommand property to Bind to in the parent view model:
<KeyBinding Gesture="CTRL+C" Command="{Binding Copy, Mode=OneWay}" />
The first option involves creating a BaseViewModel class that has an abstract ICommand property in it... any class that extends the BaseViewModel class has to implement this command... then you could have the child view model property of this type and simply pass the value along:
public override ICommand Copy
{
get { return new ActionCommand(action => ViewModel.Copy.Execute(null),
canExecute => ViewModel.Copy != null); }
}
I doubt that you want that command on every view model though, so let's investigate option two. The basic idea is that if you have access to the child view model, you can 'pass' the command along to the child:
public ICommand Copy
{
get { return new ActionCommand(action => childViewModel.Copy.Execute(action),
canExecute => childViewModel.Copy.Execute(canExecute)); }
}
ActionCommand is a custom class that is based on the common RelayCommand.
UPDATE >>>
Ok, so I've never used CommandBindings before because it's so much easier using my ActionCommand, or RelayCommand, but I've just investigated it on MSDN and two ideas spring to mind.
First, if you're using the ApplicationCommands.Copy command object in the child UserControl, then surely you can just use that same object in the KeyBinding in the parent view. ApplicationCommands is a static class and Copy is a static property, so wherever you call it, it's the same object:
<KeyBinding Gesture="CTRL+C" Command="ApplicationCommands.Copy" />
Failing that, you could create an ICommand property in your parent view model and either create your own Copy command, or just return the ApplicationCommands.Copy command. Then you could Bind to it from the parent view and the child view... or, you could even add it into the child view CommandBinding object from the parent view model.

Categories