In reactive UI, BindCommand can bind some control to view model's property or method, e.g. method in ViewModel that will be executed when some button in XAML was clicked.
https://www.reactiveui.net/docs/handbook/commands/binding-commands
How do I disable or enable a set of buttons when some of them was clicked?
According to docs, BindCommand should have 3rd argument that can accept some function, but can't find an example.
XAML
<Button Content="Start" x:Name="StartButton" />
<Button Content="Stop" x:Name="StopButton" IsEnabled="False" />
<Button Content="Pause" x:Name="PauseButton" IsEnabled="False" />
XAML.cs
// How to enable Stop and Pause when Start was clicked?
this.BindCommand(ViewModel, vm => vm.Stop, view => view.StopButton).DisposeWith(container);
this.BindCommand(ViewModel, vm => vm.Start, view => view.StartButton).DisposeWith(container);
this.BindCommand(ViewModel, vm => vm.Pause, view => view.PauseButton).DisposeWith(container);
// In plain WPF I could do modify controls inside OnClick handler
private void OnStartClick(object sender, RoutedEventArgs e)
{
// How can I do this in Reactive UI?
StopButton.IsEnabled = true;
PauseButton.IsEnabled = true;
}
View Model
public DashboardViewModel(IScreen screen)
{
HostScreen = screen;
// Or how to get access to controls in these event handlers?
Stop = ReactiveCommand.Create(() => {});
Start = ReactiveCommand.Create(() => {});
Pause = ReactiveCommand.Create(() => {});
}
ReactiveCommand.Create accepts an IObservable<bool> that determines whether the value of CanExecute:
Start = ReactiveCommand.Create(() => { });
Stop = ReactiveCommand.Create(() => { }, Start.Select(_ => true));
Pause = ReactiveCommand.Create(() => { }, Start.Select(_ => true));
Considering that 3 persons, including me, voted for creating relevant properties in View Model and binding them in XAML, I did this first.
View Model
public ReactiveCommand<Unit, Unit> StopCommand { get; protected set; }
public ReactiveCommand<Unit, Unit> StartCommand { get; protected set; }
public bool StopState { get => _stopState; set => this.RaiseAndSetIfChanged(ref _stopState, value); }
public bool StartState { get => _startState; set => this.RaiseAndSetIfChanged(ref _startState, value); }
StopCommand = ReactiveCommand.Create(() =>
{
StopState = false;
StartState = true;
});
StartCommand = ReactiveCommand.Create(() =>
{
StopState = true;
StartState = false;
});
XAML
<Button Content="Start" IsEnabled="{Binding Path=StartState}" x:Name="StartButton" />
<Button Content="Stop" IsEnabled="{Binding Path=StopState}" x:Name="StopButton" />
That seemed like the most MVVM approach, even though not exactly a Reactive UI approach. Then, I found this answer that seems to be way more elegant and doesn't require hardcoded bindings between XAML and View Model.
What are the distinctions between the various WhenAny methods in Reactive UI
Using WhenAnyObservable I can subscribe to selected command and modify XAML from the code-behind, without creating a bunch of unnecessary properties in the View Model
this
.BindCommand(ViewModel, vm => vm.StartCommand, view => view.StartButton)
.WhenAnyObservable(o => o.ViewModel.StartCommand)
.Subscribe(o =>
{
StartButton.IsEnabled = false;
StopButton.IsEnabled = true;
})
.DisposeWith(container);
Done.
Related
Please help me on this issue in WPF using MVVM
I Have a drop down ,In that 5 values were there. Onload the drop down needs to be displayed only enabling the first value and remaining needs to be disable.Once i click/check the first check box under the drop down it needs to be enable all other disabled check boxes values and also the selected value from the drop-down needs to displayed in the same combo box(multiselect) separated by comma or any delimiters using wpf MVVM architecture.
Here's something that approximately does what you want. Define a view model for the individual items in the drop down (ComboBox):
public class ChoiceViewModel : ViewModelBase
{
private string _content;
public string Content
{
get => _content;
set => Set(ref _content, value);
}
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => Set(ref _isChecked, value);
}
private bool _isEnabled;
public bool IsEnabled
{
get => _isEnabled;
set => Set(ref _isEnabled, value);
}
}
The ViewModelBase class comes from the MVVM Light Toolkit. It defines the Set method I used above, which raises the INotifyPropertyChanged event whenever the property is changed.
Next, define a view model to contain the list of choices:
public class ViewModel : ViewModelBase
{
public ViewModel()
{
Choices = new ObservableCollection<ChoiceViewModel>
{
new ChoiceViewModel {Content = "Check to enable the others", IsEnabled = true},
new ChoiceViewModel {Content = "Choice 1", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 2", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 3", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 4", IsEnabled = false},
};
Choices[0].PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(ChoiceViewModel.IsChecked))
{
var choice = (ChoiceViewModel)sender;
for (var i = 1; i < Choices.Count; i++)
Choices[i].IsEnabled = choice.IsChecked;
RaisePropertyChanged(nameof(ChoicesDisplay));
}
};
for (var i = 1; i < Choices.Count; i++)
{
Choices[i].PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(ChoiceViewModel.IsChecked))
RaisePropertyChanged(nameof(ChoicesDisplay));
};
}
}
public ObservableCollection<ChoiceViewModel> Choices { get; }
public string ChoicesDisplay =>
CheckedChoices.Any()
? string.Join(", ", CheckedChoices.Select(x => x.Content))
: "No choices made";
private IEnumerable<ChoiceViewModel> CheckedChoices =>
Choices.Skip(1).Where(x => x.IsChecked);
}
The view model handles enabling or disabling the other choices when the first choice is checked or unchecked. It also handles updating the display for the choices selected, separated by commas.
In the Window class (or whatever is appropriate for your situation), set the DataContext to the ViewModel:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
The XAML looks like this:
<ComboBox
ItemsSource="{Binding Choices}"
Text="{Binding ChoicesDisplay, Mode=OneWay}"
IsEditable="True"
IsReadOnly="True"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox
Content="{Binding Content}"
IsChecked="{Binding IsChecked}"
IsEnabled="{Binding IsEnabled}"
/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This solution does what you want, but there are some things you may want to fix. For example, even though a check box is disabled (and therefore you can't check or uncheck it), you can still choose it in the drop down.
i have a comboxbox that while it is beign populated i want it replaced in the UI by a message saying it is being loaded.
i did this by using a textbox showing the message and giving both objects visibility bindings in the view model (IsShowAuthComboBox &LoadingAuthenticationMsg)
here's the XAML code
<ComboBox x:Name="ComboBoxAuthSource"
Grid.Row="3"
Style="{StaticResource ComboBoxStyle}"
SelectedItem ="{Binding SelectedAuthenticationSource,UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding AuthenticationSource,UpdateSourceTrigger=PropertyChanged}"
Visibility= "{Binding IsShowAuthComboBox, Converter={StaticResource BoolToVis}}" />
<TextBox x:Name="ComboBoxAuthCover"
Grid.Row="3" Grid.Column="{StaticResource TableColumn}"
Style="{StaticResource FieldBoxStyle }"
FontSize="12"
IsReadOnly="True"
Visibility="{Binding IsShowGettingAuthenticationMsg, Converter={StaticResource BoolToVis}}"
Text="{Binding LoadingAuthenticationMsg,UpdateSourceTrigger=PropertyChanged,Mode=OneWay,FallbackValue='Loading authentication sources...'}" />
And here's the viewModel
public bool IsShowAuthComboBox
{
set
{
if (_isShowAuthenticationComboBox != value)
{
_isShowAuthenticationComboBox = value;
OnPropertyChanged("IsShowAuthComboBox");
OnPropertyChanged("IsShowGettingAuthenticationMsg");
}
}
get =>_isShowAuthenticationComboBox;
}
public bool IsShowGettingAuthenticationMsg => !_isShowAuthenticationComboBox;
public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
Log.Write(LogClass.General, LogLevel.Debug,
$"{propertyName} update triggerd",
_moduleName);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
this code is the first thing that happens in the relevant flow, but i will sometimes only see it at the very end of the execution and for only for an instant.
at other times it will work as expected.
what am i missing here?
EDIT :
this also accurs when validating the IP ,simpler code.
here's the code
public string SelectedServer
{
get => _selectedServer;
set
{
lock (_lockObj)
{
IsShowAuthComboBox = false;
if (!IsValideIp(value))
//some code
IsShowAuthComboBox = true;
}
}
bool IsValideIp(string ip)
{
//some code
//calls the server sync
return RemotingConfigurator.GetServerConfig(ip).isValid;
}
Your issue is that you are setting the IsShowAuthComboBox property and calling the IsValideIp synchronously on the same thread. And a single thread cannot both update the UI and query a database simultaneously.
What you should do is to call the IsValideIp on a background thread. I wouldn't do this in the setter of a property though, but rather in a command. You may want to read #Stephen Cleary's blog post on the subject.
this is what i ended up doing. moved the UI changes away from the data layer and into the viewModel (SetUiOnWait)
public string SelectedServer
{
get => _selectedServer;
set
{
//IsShowAuthComboBox = false;
SetUiOnWait(true);
Log.Write(LogClass.General, LogLevel.Debug,
$"Server changed from {_selectedServer} to {value} by user",
_moduleName);
_selectedServer = value;
OnPropertyChanged();
// OnPropertyChanged();
//workaround for when changing servers when a unique
//authentication source is selected causes the selected source to be null :\
if (AuthenticationSource.Any())
{
SelectedAuthenticationSource = AuthenticationSource[0];
}
Task.Factory.StartNew(() =>
{
LoginInfo.SelectedServer = _selectedServer;
}).ContinueWith((t) =>
{
if(t.Exception !=null)
{
ExceptionLog.Write(t.Exception.GetBaseException(),_moduleName);
}
RefreshAuthenticationProperties();
OnPropertyChanged("IsLimitedClinicalUse");
OnPropertyChanged("IsNotForClinicalUse");
SetUiOnWait(false);
});
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
dispatcher.Invoke((Action)(() =>
{
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}));
}
Task.Factory.StartNew() forces and logic to be executed on a new thread and for the UI changes to wait for it be completed.
and invoke within OnPropertyChange forces the event to be handled by the UI thread.
I have a MVVM Light WPF project that I'm working on.
I want to update the text via binding on the label when a button is clicked.
Not really sure how to do this within the view model.
Below is a look at my view code and view model code. Basically, I want to update the label with the ProjectStatus binding to say Project Created after the `Create New Project' button is clicked.
Any help would be appreciated.
Here is my code:
<Button Content="Create New Project" Margin="0,0,3,0" Command="{Binding AddProjectCommand}" Width="243"/>
<Label Margin="20,0,0,0" Content="{Binding ProjectStatus, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="325"/>
Here is the Model Code:
public RelayCommand AddProjectCommand { get; set; }
public ProjectConfigViewModel()
{
_projectStatus = "Project not created";
this.AddProjectCommand = new RelayCommand(() => AddProject());
}
public void AddProject()
{
DatabaseInteraction.CreateProjectDb(_projName);
isProjectLoaded = false;
}
public string ProjectStatus
{
get { return _projectStatus; }
set
{
if (value != _projectStatus)
{
_projectStatus = value;
RaisePropertyChanged("ProjectStatus");
}
}
}
Why not just like that?
public void AddProject()
{
DatabaseInteraction.CreateProjectDb(_projName);
isProjectLoaded = false;
ProjectStatus = "Project Created";
}
I´m trying to use a MahApps Spin control in a login wpf form.
But the caliburn binding did not work.
<Controls:ProgressRing IsActive="{Binding Busy}" Grid.Row="6" Grid.Column="1" x:Name="Busy"/>
And my model has something like
bool _busy;
public bool Busy
{
get
{
return _busy;
}
set
{
_busy = value;
NotifyOfPropertyChange(() => _busy);
}
}
public void Login(string username, string password)
{
try
{
Busy = true;
...
But it not show the Spin control on the wpf form
thanks
Looking at your notification, you are notifying on the field rather than the property.
Change
NotifyOfPropertyChange(() => _busy);
to
NotifyOfPropertyChange(() => Busy);
The problem that I am having is a little hard to describe, so please hear it out.
I'm simply opening one window from another and then trying to close the second one. If I use Command of the InputBindings of the second one, the second one closes fine. If I call the close directly it closes both the first and second window. I expect code will help is this scenario.
WPF: Window1View (key part)
<Grid>
<Button Content="Button" Command="{Binding RptKeyDownCommand}" />
</Grid>
Window1ViewModel: (shortened for listing)
using GalaSoft.MvvmLight.Command;
var _runCommand = new RelayCommand(() => Run(), () => CanRun());
public void Run()
{
var v = new Window2();
var vm = new Window2ViewModel();
vm.RequestClose += v.Close;
v.DataContext = vm;
v.ShowDialog();
}
public event Action RequestClose;
var _closeCommand = new RelayCommand(() => Close(), () => CanClose());
public void Close()
{
if (RequestClose != null)
RequestClose();
}
WPF: Window2View
<Window.InputBindings>
<KeyBinding Key="Escape" Command="{Binding CloseCommand}" />
</Window.InputBindings>
<TextBox Text="Hello">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewKeyDown">
<cmd:EventToCommand
Command="{Binding Close2Command, Mode=OneWay}"
PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
Window2ViewModel: (has the same Close Command plus EventToCommand end point)
var _close2Command = new RelayCommand<KeyEventArgs>(p => Close2(p), p => CanClose2(p));
public void Close2(KeyEventArgs e)
{
if (e.Key == Key.Escape)
Close(); <- Here closes both Window1View and Window2View?
}
See this answer on your other thread for a solution.
From Window2ViewModel you should Call RequestClose not Close.
Here is the code for Window2ViewModel
RelayCommand _close2Command;
public ICommand Close2Command
{
get
{
if (_close2Command == null)
{
_close2Command = new RelayCommand(param => CloseEx(), param => CanClose());
}
return _close2Command;
}
}
public virtual void CloseEx()
{
Close();
}
public event Action RequestClose;
public virtual void Close()
{
if (RequestClose != null)
{
*RequestClose();*
}
}
public virtual bool CanClose()
{
return true;
}
Also Window1ViewModel should have code as :
using GalaSoft.MvvmLight.Command;
var _runCommand = new RelayCommand(() => Run(), () => CanRun());
var vm;
public void Run()
{
var v = new Window2();
vm = new Window2ViewModel();
vm.RequestClose += CloseV2;
v.DataContext = vm;
v.ShowDialog();
}
public event Action RequestClose;
var _closeCommand = new RelayCommand(() => Close(), () => CanClose());
public void CloseV2()
{
vm.Close();
}
public void Close()
{
if (RequestClose != null)
RequestClose();
}
Try to understand your code.
Note in you code you are binding event of both V1.RequestClose to V2.RequestClose to same method Close. In my case I have them separate and V2.RequestClose will always call vm.Close.
hope this helps.