I am loving Template10 so far, very nice.
I am a little stuck though on how to bind to a Setting value on the Main Page.
I have added a new bool setting which is storing properly.
On my main page I have a Visibility binding to the setting:
Visibility="{Binding UseAmbientLightSensor, Converter={StaticResource CollapsedWhenFalseConverter}}"
This works on app start as expected, the MainPageViewModel reads the value from Settings and a grid is visible or collapsed based on that setting.
However I cannot seem to get this binding to 'listen' to the setting, if I go to the settings page and change that value, when I go back to the Main Page the visibility does not change. It only works if I restart the app.
In the vanilla Template10 install this would be akin to Binding a little logo on MainPage to the 'UseLightThemeButton' setting in the Settings page which changes based on that setting..
Okay, so I guess this is the "official" answer. But many approaches are valid. This one matches most closely to the templates. I would do it like this:
public class MainPageViewModel : ViewModelBase
{
Services.SettingService.SettingService _SettingService;
public MainPageViewModel()
{
_SettingService = Services.SettingService.SettingService.Instance;
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)
{
Windows.Storage.ApplicationData.Current.DataChanged += SettingsChanged;
await Task.CompletedTask;
}
public override async Task OnNavigatedFromAsync(IDictionary<string, object> pageState, bool suspending)
{
Windows.Storage.ApplicationData.Current.DataChanged -= SettingsChanged;
await Task.CompletedTask;
}
private void SettingsChanged(Windows.Storage.ApplicationData sender, object args)
{
RaisePropertyChanged(nameof(FontSize));
}
public double FontSize { get { return _SettingService.FontSize; } }
}
With that view-model, you can easily bind to a setting (in this case FontSize).
Best of luck.
There are two possible scenarios that may not be happening:
Raise the property change event when your bool value gets updated.
Set the binding to a two way mode.
In order to do this change the binding mode of your Visibility property
Visibility="{Binding UseAmbientLightSensor, Mode=TwoWay, Converter={StaticResource CollapsedWhenFalseConverter}}"
This will tell xaml to listen to any change on the property in the view model.
Then you need to tell the View model when to let the XAML view know of its changes, if you are using Template10, then it can be done as follows:
private bool useAmbientLightSensor;
public TodoListControlViewModel UseAmbientLightSensor
{
get
{
return this.useAmbientLightSensor;
}
set
{
this.Set(ref this.useAmbientLightSensor, value);
}
}
The view model needs to extend from the ViewModelBase class which provides the Set method that raises the OnPropertyChanged event, allowing the view to know of any change in the view model.
For more info, check the INotifyPropertyChanged interface and its implementation.
Related
I am new to WPF and MVVM (coming in from WinForms and Events), so please bear with me!
I am trying to figure out how to use the same INotifyPropertyChanged value binding between multiple views. I am using MVVM Light. I have ViewModels that inherit from ViewModelBase backing my Views (with no code behind). I'm not sure how to explain the issue, but I think an example will make it clear what I'm after.
I have a main window. It has a standard TabControl. On the Login TabItem I have a custom login control. Below the TabControl, I have a custom status bar control. The desired behavior is that when the user logs in, the status bar is updated with their login status and name, and the other TabItems on the main window become enabled (they should be disabled when not logged in).
So to summarize I have:
MainWindow (view) with MainWindowViewModel
Login (view) with LoginViewModel (in TabControl of MainWindow)
StatusBar (view) with StatusBarViewModel (at bottom of MainWindow)
Here is what my StatusBarViewModel looks like:
public class StatusBarViewModel : ViewModelBase, IStatusBarViewModel
{
private bool _isLoggedIn;
public bool IsLoggedIn
{
get { return _isLoggedIn; }
set { Set(ref _isLoggedIn, value); RaisePropertyChanged(); }
}
// other properties follow
}
I inject (using Ninject) the (singleton) concrete instance of IStatusBarViewModel into the LoginViewModel via constructor injection:
public class LoginViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public LoginViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
And I do the same for the MainWindowViewModel:
public class MainWindowViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public bool IsLoggedIn => _statusBar.IsLoggedIn;
public MainWindowViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
Note: I think this is where my problem is... Not sure if MVVM Light interprets this as a bindable property and applies the proper change notifications. If I add a setter (which I don't need here), that won't work because A property or indexer may not be passed as an out or ref parameter. So I'm unclear on what is going on when I do this.
Back on track here, so when the login is successful, I am able to update the IsLoggedIn property from the LoginViewModel like so:
_statusBar.IsLoggedIn = true;
I set up the binding in my MainWindow XAML like so:
<TabItem Header="Event" IsEnabled="{Binding IsLoggedIn}">
<views:Events/>
</TabItem>
The binding works correctly when the view is first loaded, but subsequent changes to the property don't trigger a change in IsEnabled. The StatusBar (view) however does update accordingly.
I had tossed around the idea of injecting references to both the StatusBarViewModel and the MainWindowViewModel in to my LoginViewModel (and then having to set two properties after login), but that made me think that I'm not approaching this correctly because I'm creating dependencies.
So basically the question is:
Is my approach correct, per the MVVM pattern?
Am I on the right track and just need to modify the code a bit?
If not, what is the (or a) standard pattern to handle this scenario?
Your guess is correct. The problem is here:
public bool IsLoggedIn => _statusBar.IsLoggedIn;
... because it's not going to generate the change notification. What you could do is just expose the IStatusBarViewModel via a public property and then bind to its own IsLoggedIn property directly.
In the ViewModel:
public class MainWindowViewModel : ViewModelBase
{
private IStatusBarViewModel _statusBar;
public IStatusBarViewModel StatusBar => _statusBar;
public MainWindowViewModel(IStatusBarViewModel statusBar)
{
_statusBar = statusBar;
}
}
And in the View:
<TabItem Header="Event" IsEnabled="{Binding StatusBar.IsLoggedIn}">
<views:Events/>
</TabItem>
I have seen quite a few different posts regarding this, but am still confused as to what the proper way to do this is. I am implementing RelayCommand and IRequireViewID to do things like call a WindowsManager class to close windows from the view where it doesn't need to know what window it is calling, etc.
However, situations that arise like this is where I am unsure how to implement things properly.
So basically I have a command in the viewModel where you click a button and I need to save data to a DB once this happens. How do I have the command in the View but then put the logic for this into the ViewModel? IE, basically the ViewModel would need to know when the Command is called. I mean I could always put a static method in the viewmodel and call it from the view but I am assuming that probably is not a good way to implement it, ie MyViewModel.RedValidation from the view.
RelayCommand Class
public class RelayCommand : ICommand
{
private Action commandTask;
public RelayCommand(Action commandToRun)
{
commandTask = commandToRun;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
commandTask();
}
}
View
public ICommand ValidateRed
{
get => new RelayCommand(RedValidation);
}
private void RedValidation()
{
//SAVE Data To DB <----This should be in the ViewModel though right?
}
XAML:
<Button Name="ValidateBtn" Style="{StaticResource mainButtons}" Content="Validate Email" Width="100"
HorizontalAlignment="Left" Command="{Binding ValidateRed}"/>
Everything you have in "View" should be in "ViewModel". Commands are properties of the view model, not the view (also, you shouldn't be binding against the view 99% of the time).
Once you have changed that, it should fall into place the way you expect.
I am trying to show loading screen when a network request is made. I have a LoginViewModel where I have defined a property IsLoading. Now when it changes I want to display activity indicator. I am trying to use BTProgressHUD here. Although I have been able to bind my controls with the ViewModel but I am not able to bind BTProgressHUD as show or hide. Following is how I am binding other controls:
var set = this.CreateBindingSet<LoginView, LoginViewModel>();
set.Bind(txtUser).To(vm => vm.Username);
set.Bind(txtPassword).To(vm => vm.Password);
set.Bind(btnLogin).To(vm => vm.LoginCommand);
set.Apply();
i have tried following for BTProgressHUD but it doesn't work:
set.Bind(BTProgressHUD.Show()).To(vm => vm.IsLoading);
I am not able to compile as I am getting error for Bind function. Is it possible to create a closure(like in iOS) bound with a property in ViewModel and for any change in property can do some operation in View?
I presume your LoginCommand calls a login function. In that function you should call BTProgressHUD.Show(), followed by your login process.
You should wrap your login process in a try/catch and in the finally you should call BTProgressHUD.Dismiss().
This has the added benefit of not having to repeat the code in all of your platforms.
As an extra, you could create a BaseViewModel which your view models inherit from, give it an IsLoading property, and set your BTProgressHUD.Show() and BTProgressHUD.Dismiss() whenever it is called.
private bool _isLoading = false;
public virtual bool IsLoading
{
get { return _isLoading; }
set
{
SetProperty(ref _isLoading, value);
if (value)
BTProgressHUD.Show();
else BTProgressHUD.Dismiss();
}
}
Beginner here - I'm struggling to wrap my head around accessing viewmodel properties. In my particular case, I have a solution containing two different projects. One contains the UI components, the other the majority of the "work."
My viewmodel in my UI project contains the following property:
private int _createfileprogress { get; set; }
public int CreateFileProgress
{
get { return _createfileprogress ; }
set
{
if (value == _createfileprogress )
return;
_createfileprogress = value;
OnPropertyChanged("CreateFileProgress");
}
}
The purpose of this int property is to fill in the progress of a progress bar. I'd like to reference it in my other project, where a long-running method is executed as such:
public void LongRunningMethod()
{
CreateFileProgress= 0;
// Do some calculations
CreateFileProgress= 10
// More calculations and external calls
CreateFileProgress= 20
// etc.
}
But I can't find the right way to connect these two. I'm sure doing this wrong - would appreciate any guidance. Thanks!
I hope i understand your question.
Your ViewModels and the View are in the same Project und you want to monitor the progress from the Model which is in an another Project?
i think you are searching for Events / Observer Pattern
In MVVM the Model didn't care about the ViewModel and the View.
The Prober Way in MVVM is that the model raise Events which the Viewmodel can subscribe.
General Event Example in .Net
You can create an EventArgs Class like
public class ProgressEventArgs :EventArgs
{
public int CurrentProgress { get; }
public ProgressEventArgs(int progress)
{
CurrentProgress = progress;
}
}
and create the Event in your Model (LongRunningMethod) Class
public event EventHandler<ProgressEventArgs> ProgressChanged;
protected void OnProgressChanged(int progress)
{
ProgressChanged?.Invoke(this, new ProgressEventArgs(progress));
}
So your Method can raise Events
public void LongRunningMethod()
{
OnProgressChanged(0);
// Do some calculations
OnProgressChanged(10);
// More calculations and external calls
OnProgressChanged(20);
// etc.
}
which the ViewModel subscribe
public class ProgressViewModel
{
public ProgressViewModel()
{
var model = new Model();
model.ProgressChanged += (sender, e) => {
//invoke ui thread
Application.Current.Dispatcher.Invoke(
new Action(() =>
{
CreateFileProgress = e.CurrentProgress;
}));
};
}
}
It seems that what might best help you would be to set the DataContext of your UI element (either the Window, the Page, or the ProgressBar itself) to your ViewModel, which will allow your UI to be bound to ViewModel properties. Since your ViewModel seems to implement the INotifyPropertyChanged interface, your OnPropertyChanged() method should automatically keep the UI up-to-date when you use DataBinding.
You'll first want to make a reference to your class library project that contains the "work". This will allow your "UI project" to access the resources (namely, the ViewModel) from the "work project". Do this by right-clicking References under your "UI Project" in the Solution Explorer, choose Add Reference..., find the Projects tab on the left side, then check the box for your "work project". Apply it and your "UI project" can now access your "work project".
Once that is done, you can use either of the following methods to produce the result I believe you're looking for:
Set the Window's DataContext to your ViewModel
(Best for when many elements within your Window will use resources from your ViewModel)
<Window
//Some attributes
//Some more attributes
xmlns:vm="clr-namespace:MyWorkProject.ViewModels;assembly=MyWorkProject">
<Window.DataContext>
<vm:MyViewModel/>
</Window.DataContext>
<Grid>
<ProgressBar Value="{Binding CreateFileProgress}/>
</Grid>
</Window>
OR
Set only the ProgressBar's DataContext using a StaticResources
(Best if you don't need the entire Window's DataContext to be the ViewModel)
<Window
//Some attributes
//Some more attributes
xmlns:vm="clr-namespace:MyWorkProject.ViewModels;assembly=MyWorkProject">
<Window.Resources>
<vm:MyViewModel/>
</Window.Resources>
<Grid>
<ProgressBar Value="{Binding Source={StaticResource MyViewModel}, Path=CreateFileProgress}/>
</Grid>
</Window>
I've created a property "IsLoading" for my main view model. The idea is that a progressbar is displayed whenever this property is set to true. So far so good
The catch is, that I have a command, that calls another viewmodel (the code is there because it's a functionality from another page, but I want to be able to shortcut it as well from my main viewmodel)
So, I went ahead and modified the main property to something like this :
public const string IsLoadingPropertyName = "IsLoading";
private bool _isLoading;
public bool IsLoading
{
get
{
return _isLoading || ((ViewModelLocator)Application.Current.Resources["Locator"]).SettingsViewModel.IsLoading;
}
set
{
if (value != _isLoading)
{
_isLoading = value;
RaisePropertyChanged(IsLoadingPropertyName);
}
}
}
and the xaml
<shell:SystemTray.ProgressIndicator>
<shell:ProgressIndicator IsIndeterminate="true" IsVisible="{Binding Main.IsLoading, Source={StaticResource Locator}}" />
</shell:SystemTray.ProgressIndicator>
So, I'm saying that main view model is loading when there's something loading there, or if the settings view model is loading.
The problem is that the binding only works when setting the main view model's IsLoading property, it doesn't react when I set it in the inner IsLoading one. Both have the same property name "IsLoading". Shouldn't it be detected?
For example, in Main view model (just the execution of the command for simplicity) :
private void ExecuteRefreshCommand()
{
ViewModelLocator viewModelLocator = Application.Current.Resources["Locator"] as ViewModelLocator;
viewModelLocator.SettingsViewModel.GetCurrentLocationCommand.Execute(null);
}
and inside the settings view model :
public RelayCommand GetCurrentLocationCommand
{
get
{
Action getLocation = () =>
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
return;
}
var watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
watcher.PositionChanged += WatcherPositionChanged;
IsLoading = true; // settings view model "IsLoading" propertychanged raising property
watcher.Start();
};
return new RelayCommand(getLocation);
}
}
You're looking at the MainViewModel's isLoading property to determine whether to show the progressbar or not. Silverlight uses the NotifyPropertyChanged event to determine when it should reevaluate a certain property. When setting either the SettingsViewModel's IsLoading property- or the MainViewModel's property, you only raise the changedEvent for that ViewModel. You should raise the ChangedEvent for both.
A modified setter example could be (depending on the exposed methods)
set
{
if (value != _isLoading)
{
_isLoading = value;
RaisePropertyChanged(IsLoadingPropertyName);
((ViewModelLocator)Application.Current.Resources["Locator"]).SettingsViewModel.RaisePropertyChanged(IsLoadingPropertyName);
}
}
Note that many MVVM frameworks offer a functionality called Messaging which is ideal to do cross ViewModel communication without creating the strict dependency you created right now. Alternatively you can use a globally consumed IsLoading property.