Calling a method in one viewmodel from a different viewmodel - c#

I'm developing with MVVMLight on a Windows Phone 8.1 app. We have a setting for sorting a list of users by first or last name. After changing this setting (performed by a ListPicker binded to a property in SettingsViewModel), I want to call a method in a different view model (OtherViewModel) to re-sort a list of users on OtherViewModel's corresponding view. A settingsStore is being used to store the sort setting on the user's phone.
I'd prefer not to create view model dependencies by way of var vm = new ViewModel(), since there is not a parent/child relationship between the two view models. I've been told a delegate would be a good choice, but I'm not very familiar with how I'd implement a solution using a delegate.
Any help would be appreciated.
Settings View
<toolkit:ListPicker x:Name="ContactsSortParametersListPicker"
ItemsSource="{Binding ContactsSortParameters, Mode=OneTime}"
SelectedItem="{Binding ContactsSortParametersSelected, Mode=TwoWay}"
SelectionChanged="ContactsSortParametersListPicker_SelectionChanged"/>
Settings View (code-behind)
private void ContactsSortParametersListPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// want to call method from MainViewModel that updates a list in Main View
}
SettingsViewModel
public IEnumerable<SortOptions> ContactsSortParameters
{
get { return (IEnumerable<SortOptions>)Enum.GetValues(typeof(SortOptions)); }
}
private SortOptions _sortContactsParameterSelected;
public SortOptions ContactsSortParametersSelected
{
get { return _sortContactsParameterSelected; }
set
{
SetProperty(ref _sortContactsParameterSelected, value);
_settingsStore.ContactsSortParameter = _sortContactsParameterSelected;
}
}
OtherViewModel
public async Task LoadDirectory()
{
...relevant logic here...
}

If you use MVVM Light, I assume that you have ViewModelLocator instance in your App.xaml resources defined like below.
<vm:ViewModelLocator xmlns:vm="clr-namespace:WPApp.ViewModel" x:Key="Locator" />
In your settings view code behind:
private async void ContactsSortParametersListPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
await ((ViewModelLocator)App.Current.Resources["Locator"]).OtherViewModel.LoadDirectory();
}

Related

ICollectionView ui is not updated

I have ObserveCollection (entity) associated with ICollectionView
Everything works fine until I try to delete the entry. After clicking on the 'delete' button, the interface is not updated.
If i set ObserveCollection everything works fine
private ICollectionView _taskview;
public ICollectionView TasksView
{
get { return _taskview; }
set
{
_taskview = value;
OnPropertyChanged("TaskView");
}
}
public ICommand DeleteTask
{
get
{
return new DelegateCommand(() =>
{
_context.Task.Attach(SelectTask);
_context.Task.Remove(SelectTask);
_context.SaveChanges();
Tasks = new ObservableCollection<TaskModel>(_context.Task);
TasksView = CollectionViewSource.GetDefaultView(Tasks);
});
}
}
public HomeViewModel(Window window)
{
this.window = window;
Tasks = new ObservableCollection<TaskModel>(_context.Task);
TasksView = CollectionViewSource.GetDefaultView(Tasks);
}
<ListBox Grid.Row="1" Grid.RowSpan="2" Grid.Column="0"
SelectionMode="Extended"
ItemsSource="{Binding TasksView}"
SelectedItem="{Binding SelectTask}">
</ListBox>
Don't create a new collection after each deletion. This will have negative impact on the performance. This is the reason why you use an ObservableCollection. This way the binding target e.g., a ListBox is able to update the changed items only, instead of recreating/rendering the complete view.
In this this context it also doesn't make sense to expose the data source as ICollectionsView. Rather bind to the ObservableCollection directly.
When the source collection of an ICollectionsView implements INotifyCollectionChanged like ObservableCollection<T> does, then the ICollectionView will automatically update when the source changes.
In this case manipulating the INotifyCollectionChanged collection is sufficient.
When the source collection of an ICollectionsView does not implement INotifyCollectionChanged like List<T>, then the ICollectionView will not automatically update when the source changes.
In this case you must explicitely call ICollectionView.Refresh to force the ICollectionView to update.
Please note that you should never reference any view components in your view model - no exceptions. This eliminates all benefits of MVVM. And it is never necessary, for sure. If your view model requires a reference to a view component that you are designing your code or classes wrong.
To follow this basic and fundamental MVVM design rule you must remove the reference to Window from your HomeViewModel.
You can trigger view behavior by exposing a property on the view model which is the input for a data trigger in the view. Patterns - WPF Apps With The Model-View-ViewModel Design Pattern, The Model-View-ViewModel Pattern.
First Solution (Recommended)
You should bind to the Tasks collection directly.
The moment you need to manipulate the collection's view e.g., to apply a filter retrieve the view using CollectionViewSource.GetDefaultView(Tasks). But don't bind to it.
<ListBox ItemsSource="{Binding Tasks}" />
public HomeViewModel()
{
Tasks = new ObservableCollection<TaskModel>(_context.Task);
Tasks.CollectionChanged += OnTasksChanged;
}
private void OnTasksChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
foreach (TaskModel task in e.NewItems)
{
_context.Task.Add(task);
_context.SaveChanges();
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
foreach (TaskModel task in e.OldItems)
{
_context.Task.Attach(task);
_context.Task.Remove(task);
_context.SaveChanges();
}
break;
}
}
}
// Then simply manipulate the 'Tasks' collection
public ICommand DeleteTaskCommand => new DelegateCommand(() => Tasks.Remove(SelectTask));
Second Solution
If you want to bind to the ICollectionView, you don't need the additional ObservableCollection anymore (except you want to maintain two collections and a ICollectionView on every add/move/remove/reset operation). To update the collection's view call ICollectionView.Refresh.
<ListBox ItemsSource="{Binding TasksView}" />
public HomeViewModel()
{
TasksView = CollectionViewSource.GetDefaultView(_context.Task);
}
// Then simply refresh the 'TasksView':
public ICommand DeleteTask => DelegateCommand(
() =>
{
_context.Task.Attach(SelectTask);
_context.Task.Remove(SelectTask);
_context.SaveChanges();
// Update the view
TasksView.Refresh();
});
Call Refresh() on View property of CollectionViewSource to get it refreshed.
You have a typo on:
public ICollectionView TasksView
{
get { return _taskview; }
set
{
_taskview = value;
OnPropertyChanged("TaskView");
}
}
At OnPropertyChanged("TaskView"); it should be OnPropertyChanged("TasksView");

Accessing Xamarin.Forms inside ViewModel

I am very new to Cross platform development in Xamarin.forms. My code should be highly unit testable and I have created a ViewModel class to communicate between my View and the Model. I want to know, If we are going to access Xamarin.forms (using Xamarin.Forms;), inside the viewmodel, is it a bad practice or violating MVVM concept. If so How we can use Command inside ViewModel to bind with the view.
Thanks
Accessing the view from the view model is indeed "against" the MVVM principle. So, you're right on that! Binding a Command though, isn't that hard and works just as any other binding that you might've used.
In your view model, just define a property for the Command:
public class YourViewModel
{
public Command YourCommand { get; set; }
}
Then in the constructor define the logic that should be executed:
public YourViewModel()
{
YourCommand = new Command(() =>
{
Console.WriteLine("TADA!");
});
}
Of course, you could define it is it's own separate method or declare it another way. For brevity, I have done it inline for now.
To bind to it from XAML, simply do:
<Button Text="Make it so!" Command="{Binding YourCommand}" />
and make sure that you have set the BindingContext property on your page to the view model. If you are using an MVVM framework you might have already done so.
At some point, you will probably want to bind to a cell in a ListView or similar. In that case, keep in mind that the scope changes. You are then binding to the view model behind that cell and not the page. So, you will have to make a reference to the page to reach that BindingContext. But cross that bridge when you get there, just keep in mind that there is something special about it.
Use below code, so you don't need to import Xamarin.Forms in your ViewModel:
ButtonBehavior
public class ButtonBehavior : Behavior<Button>
{
protected override void OnAttachedTo(Button bindable)
{
base.OnAttachedTo(bindable);
bindable.Clicked += Bindable_Clicked;
}
private void Bindable_Clicked(object sender, EventArgs e)
{
//Use you logic here
}
protected override void OnDetachingFrom(Button bindable)
{
base.OnDetachingFrom(bindable);
bindable.Clicked -= Bindable_Clicked;
}
}
View
<Button Text="Click Me" HeightRequest="50" WidthRequest="80">
<Button.Behaviors>
<behavior:ButtonBehavior/>
</Button.Behaviors>
</Button>

WPF many same events are regitering when rendering view

I have a question, I have a view and in that view I am having combobox:
<ComboBox ItemsSource="{Binding ProjectsBrowserAxmModules}" SelectedValuePath="AxmModuleId" DisplayMemberPath="AxmModuleName"
SelectedValue="{Binding SelectedAxmModule, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
controls:TextBoxHelper.Watermark="{Binding BrowserComboBoxWatermark}" Height="2" IsSynchronizedWithCurrentItem="True"
SelectionChanged="ComboBox_CurrentBrowserAxmChanged" >
And so event looks like:
private void ComboBox_CurrentBrowserAxmChanged(object sender, RoutedEventArgs e)
{
((CurrentsHistoryViewModel)DataContext).GetCurrentsModuleCommand.Execute(sender);
}
And every time i change view to different one and back to this view it looks like this event is registering anew so for example if i go to different view go back and go different view and back again this event will fire 4 times.
I tried
Loaded -= ComboBox_CurrentBrowserAxmChanged;
But no luck is there any way to unregistered those redundant events.
I believe, combobox SelectedValue property gets changed somehow and internally it calls your combobox selection changed event. have debugger point in setter of SelectedAxmModule property. hope you will find given property is hitting when you are switching screens.
My suggestion is , to remove selectionChanged event. and use event/delegate for same purpose.
Code Snipt:
public class AxmModule: NotifierModel
{
public static event MyEventHandler ValueChanges;
public delegate void MyEventHandler(string Value);
private string _selectedAxmModule ;
public string SelectedAxmModule
{
get
{
return _selectedAxmModule ;
}
set
{
_selectedAxmModule = value;
if (ValueChanged!= null)
{
ValueChanged(_selectedAxmModule );
RaisePropertyChanged("SelectedAxmModule ");
}
}
}
}
Register it in your view Code behind/ viewmodel
AxmModule.ValueChanged+= AxmModule_ValueChanged;
public void AxmModule_ValueChanged(string value)
{
// your code
}
Hope, this will resolve your issue.

c# WPF MVVM TabControl with Multiple ViewModels and changing tabs

So I currently have a Window with a TabControl. The MainWindow has its own ViewModel and all the TabItems have their own ViewModels also.
I can easily change tabs from the MainWindow ViewModel through a bound SelectedIndex property. What I would like to do is change to another tab from code that runs within ANOTHER tab viewmodel. Since the Tabs are not part of the MainWindowViewModel, I am looking for a clean way to change the Tab without resorting to code behind to do it.
There are also cases, where I might need to change the tab from something such as a message prompt. I thinking my only way is to create and event and subscribe to that from MainWindowViewModel.
So I solved this with an EventAggregator.
public static class IntAggregator
{
public static void Transmit(int data)
{
if (OnDataTransmitted != null)
{
OnDataTransmitted(data);
}
}
public static Action<int> OnDataTransmitted;
}
First ViewModel sends data.
public class ModifyUsersViewModel : INotifyPropertyChanged
{
private void change_tab(int data)
{
IntAggregator.Transmit(data);
}
}
Second ViewModel receives data and then does something with it.
public class MainWindowViewModel : INotifyPropertyChanged
{
private int _Tab_SelectedIndex = 0;
public int Tab_SelectedIndex
{
get
{
return _Tab_SelectedIndex;
}
set
{
_Tab_SelectedIndex = value;
OnPropertyChanged(new PropertyChangedEventArgs("Tab_SelectedIndex"));
}
}
public MainWindowViewModel()
{
IntAggregator.OnDataTransmitted += OnDataReceived;
}
private void OnDataReceived(int data)
{
Tab_SelectedIndex = data;
}
}
Rather than trying to bind to SelectedIndex, if the TabItems have their own view models, then you can create a property for each of those view models: IsSelected and then bind the TabItem.IsSelected property to that:
<TabItem IsSelected="{Binding IsSelected}">
This prevents the view models from needing to know the index of their corresponding TabItem, something I would argue is a detail that should be specific to the view and something the view model should not concern itself with. What if you add another TabItem or want to change the order? Now you've got changes to make in the view models for something that could be just simple change to the view.

Populate Picker with Data using MVVM Xamarin Forms

I am trying to get a picker to populate with data. There doesn't seem to be a straight answer anywhere on this. I have tried numerous things, one thing that does work is :
Xaml:
<Picker Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="4"
Title="Driver Name"
ItemsSource="{Binding Drivers}"
SelectedItem="{Binding driverID}"
and in the View Model:
List<string> Drivers = new List<string> { "Steve","Dave" };
This works fine, but its just a dummy capability as in the future these names will be grabbed from a service of some kind.
So in an attempt to copy this I tried separating this list into a mock service and just returning the list to the view model and making it work that way.
But this still returns nothing to the front end even though I can see that the list is not blank when debugging.
Then I tried to create a class of Driver and return instances of the class that has the name in it and access it in the Xaml.
This did not work, I even tried a variation using IList, this did not work either.
I am not sure why this does not work as the list was just separated to essentially a different class. For instance this is what I am trying now:
Xaml:
<Picker Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="4"
Title="Driver Name"
ItemsSource="{Binding Drivers}"
SelectedItem="{Binding driverID}"
/>
View Model:
This is bound to the picker
public List<string> Drivers;
Then this method is called from the constructor:
public async Task FindDriverNames()
{
Drivers = await GetDriverNames();
}
and in the Model:
public async Task<List<string>> GetDriverNames()
{
await Sleep();
List<string> _drivers = new List<string> { "Steve"};
return _drivers;
}
This does not work, but when run through debug it shows that Drivers is populated.
I have wasted a lot of time trying to get this work, does anyone have insights?
You'll need an ObservableCollection, and possibly a INotifyPropertyChanged interface implementation to notify the view for any changes.
public class YourViewModel: INotifyPropertyChanged
{
public YourViewModel()
{
Drivers = new ObservableCollection<string>();
}
private ObservableCollection<string> _drivers;
public ObservableCollection<string> Drivers
{
get { return _drivers; }
set
{
if (Equals(value, _drivers)) return;
_drivers= value;
OnPropertyChanged(nameof(Drivers));
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
By implementing the INotifyPropertyChanged interface, you will allow the view to be updated whenever the whole list changes. The observable collection will notify the UI if a item is added to the collection.
As noted by Roman, youo can also, in this specific case use the observable collection to update the ui.
public async Task FindDriverNames()
{
Drivers.Clear();
Drivers.AddRange(await GetDriverNames());
}
For other bound properties you'll still need the OnPropertyChanged event.
See ObservableCollection<T> and INotifyPropertyChanged

Categories

Resources