Using MessagingCenter for communicating between ViewModels to Views and Navigation - c#

In a XAML Page (A View), I have a bunch of controls like Pickers that are binded to a ViewModel, called "NegotiationVM"
For example, I have a Picker binded to one of those properties "NegotiatorSelected" which returns an object "Negotiator".
My corcern is about a pair of buttons beside of that negotiator picker, are not binded to that Negotiation ViewModel or any Command, those buttons are for Adding or Editing a Negotiator. Since I'm trying to follow MVVM pattern (with no framework yet...) I would like to know what is the best practice here.
My approach is in the First ViewModel create Commands that calls methods that send an instance of the child ViewModel to the View, so when this receive it, then navigates to the child View.
public class NegotiationVM: INotifyPropertyChanged{
public NegotiationVM{
AddNegotiatorCommand = new Command(AddNegotiator,()=> !IsBusy);
}
...
public void AddNegotiator(){
negotiatorvm = new NegotiatiorVM(_client)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
public void EditNegotiator(){
negotiatorvm = new NegotiatiorVM(_client,_negotiatior)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
}
So in NegotiationView I do this:
<!-- More controls binded to NegotiationVM -->
...
<!-- -->
<Picker x:Name="pickerNegotiator" Title="Select..." ItemsSource="{Binding Negotiatorsofthisnegotiation}" SelectedItem="{Binding SelectedNegotiator}" />
<Button x:Name="btnAddNegotiator" Text="➕" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding AddNegotiatorCommand}" />
<Button x:Name="btnEditNegotiator" Text="📝" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding EditNegotiatorCommand}" />
<!-- ... -->
and in code-behind:
...
public NegotiationView(){
negotiationvm = new NegotiationVM();
this.BindingContext = negotiationvm;
MessagingCenter.Subscribe<NegotiationVM,NegociatorVM>(this,"NegotiatorVM",AddEditNegotiator);
}
...
void AddEditNegotiator(NegotiationVM arg1, NegotiatorVM vm)
{
NegotiatorPage negotiatorPage = new NegotiatorPage()
{
BindingContext = vm
};
Navigation.PushAsync(negotiatorPage);
}
So using MessagingCenter to communicate a ViewModel to a View in order to bind it to a child view and then Navigate to its view, is this a good practice considering that I'm not using any MVVM framework? Is this approach is making ViewModel from View decoupled enough? I read some comments that using MessagingCenter is not a good idea.

Related

MAUI add control element to GUI using MVVM

I use the community tool MVVM for my current MAUI project.
I would like to dynamically add controls like an entry to the GUI during runtime. I would like to do that from the ViewModel.
Using the toolkit, it is of course very easy to provide and interact with functions and properties. Unfortunately I haven't found a way to directly access a StackLayout or something similar.
I tried giving the VerticalStackLayout property x:name (in my xaml document) a name and then accessing it. This works from the code-behind, but not from the ViewModel itself.
I expected that with in the viewModel for example my StackLayout is displayed and then I can execute the following.
stackLayout.Add(new Label { Text = "Primary colors" }));
Furthermore I tried to provide a binding to the property x:name.
x:Name="{Binding StackLayout}
In the ViewModel I then tried to provide the property.
[ObservableProperty]
VerticalStackLayout stackLayout;
To clarify: the ViewModel doesn't know about the View, but the View DOES know about the ViewModel.
Thus, the view's code behind can do what is needed.
If the View doesn't already have a property holding the viewmodel, then add to code behind:
private MyVM VM => (MyVM)BindingContext;
That defines a VM property, so you can do VM.MyDictionary[someKey] or similar.
If you need to access VM in constructor BEFORE setting BindingContext,
then edit question, to show how BindingContext is set currently.
Yes, you can use MVVM to achieve this.
A simple method is to use Bindable Layouts to achieve this.
Please refer to the following code:
1.create a viewmodel for current page
MyViewModel.cs
public class MyViewModel
{
public int index = 0;
public ObservableCollection<Data> Items { get; set; }
public ICommand AddItemCommand => new Command(addItemMethod);
private void addItemMethod(object obj)
{
index++;
Items.Add(new Data { FileName ="File " + index});
}
public MyViewModel()
{
Items = new ObservableCollection<Data>();
}
}
Data.cs
public class Data
{
public string FileName { get; set; }
}
2.MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mauiapp="clr-namespace:MauiAddViewApp116"
x:Class="MauiAddViewApp116.MainPage"
x:Name="mainpage"
>
<ContentPage.BindingContext>
<mauiapp:MyViewModel></mauiapp:MyViewModel>
</ContentPage.BindingContext>
<ScrollView>
<VerticalStackLayout
Margin="10"
VerticalOptions="StartAndExpand">
<Button Text="Add item" Command="{Binding AddItemCommand}"></Button>
<StackLayout BindableLayout.ItemsSource="{Binding Items}" Orientation="Vertical">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label HorizontalOptions="Fill" Text="{Binding FileName}" FontSize="Large" HeightRequest="38" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
I have found a solution to my problem.
As you have advised me, I have put it around. I use the code-behind of my view to access the StackLayout.
1. MainPage.xaml
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center"
x:Name="VStackLayout">
</VerticalStackLayout>
</ScrollView>
With the property x:name I can access the VS layout from the code behind.
2. MainPage.xaml.cs
Dictionary<string, object> keyValuePairs = new Dictionary<string, object>();
public MainPage(MainPageViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
foreach (var item in viewModel.KeyValues)
{
if (item.Value == "String")
{
keyValuePairs.Add(item.Key, "");
var entry = new Entry {
Placeholder = item.Key,
ClassId = item.Key,
Text = (String)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
VStackLayout.Add(entry);
}
else if (item.Value == "Boolean")
{
keyValuePairs.Add(item.Key, true);
Label label = new Label { Text = item.Key};
var toogle = new Switch
{
IsEnabled = true,
ClassId = item.Key,
IsToggled = (Boolean)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
HorizontalStackLayout views = new HorizontalStackLayout();
views.HorizontalOptions = LayoutOptions.StartAndExpand;
views.VerticalOptions = LayoutOptions.Center;
views.Add(label);
views.Add(toogle);
VStackLayout.Add(views);
}
}
Here the Dic in the ViewModel is accessed and then the GUI is created from it.
Unfortunately, the access to the content of the elements (entries) does not work yet. I would like to see how to write the content in a Dictonary. The binding at this point does not work yet. Does anyone have an idea?
First of all, I want to answer that nothing is stopping you from passing a reference of your StackLayout as CommandParameter to your Command in the ViewModel. Write this:
[RelayCommand]
void Add(StackLayout myLayout)...
And just pass the reference in the XAML.
However, there are very few situations that justify this.
None of those situations are "to customize the GUI".
You need to learn how to use DataTemplates, DataTriggers, Styles, EventToCommandBehaviors, Gestures, ControlTemplates, Validators and ValueConvertors.
This will cover your basic needs for accessing the View and its elements.

Get the row details when a control in data template is selected

Im building a WPF application and trying to stick to the MVVM pattern as much as possible. I have a list box with a data template inside of it that contains a TextBlock and Button. If the button within the data template is clicked it does not select the entire row, so I am unaware of what row it pertains to. I would like to grab the entire object and bind it to a property in the view model. Can I get some help or a workaround for this please that sticks to mvvm pattern.
List box with item template
<telerik:RadListBox Width="200" Height="150" HorizontalAlignment="Left" Margin="10" ItemsSource="{Binding ListOfSupplierInvoices}"
SelectedItem="{Binding SelectedSupplierInvoice, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<telerik:RadListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" >
<TextBlock Text="{Binding InvoiceNumber}" HorizontalAlignment="Left" Margin="5" ></TextBlock>
<telerik:RadButton Height="20" >
<telerik:RadButton.Content>
<Image Source="/ImageResources/Misc/delete.png" Stretch="Fill" />
</telerik:RadButton.Content>
</telerik:RadButton>
</StackPanel>
</DataTemplate>
</telerik:RadListBox.ItemTemplate>
</telerik:RadListBox>
How it looks in the view:
As far as I understand your code, the button corresponds to a delete command, which means you want to delete the item associated with the button. In this case, the selection might not need to change, you just have to pass the current item to the delete command.
Add a Delete command to your view model like this:
public class MyViewModel : ViewModelBase
{
public MyViewModel()
{
Delete = new DelegateCommand(ExecuteDelete, CanExecuteDelete);
// ...other code.
}
public ICommand Delete { get; }
private void ExecuteDelete(object obj)
{
var invoiceItem = (InvoiceItem) obj;
// Use this only if you need the item to be selected.
// SelectedSupplierInvoice = invoiceItem;
// ...your delete logic.
}
private bool CanExecuteDelete(object obj)
{
// ...your can execute delete logic.
}
// ...other code.
}
Note that I introduced InvoiceItem as item type, because I do not know your item type, simply adapt it. The Delete command gets the current item passed as parameter. If you can always remove the item, there is no need in selecting it, as it is gone afterwards.
Otherwise, uncomment the line so the SelectedSupplierInvoice is set to the item which will automatically update the user interface through the two-way binding if you have implemented INotifyPropertyChanged correctly or derive from ViewModelBase which exposes the RaisePropertyChanged or OnPropertyChanged method, e.g.:
private InvoiceItem _selectedSupplierInvoice;
public InvoiceItem SelectedSupplierInvoice
{
get => _selectedSupplierInvoice;
set
{
if (_selectedSupplierInvoice == value)
return;
_selectedSupplierInvoice = value;
RaisePropertyChanged();
}
}
In your XAML wire the button to the Delete command on the DataContext of the RadListBox.
<telerik:RadButton Height="20"
Command="{Binding DataContext.Delete, RelativeSource={RelativeSource AncestorType={x:Type telerik:RadListBox}}}"
CommandParameter="{Binding}">
<telerik:RadButton.Content>
<Image Source="/ImageResources/Misc/delete.png" Stretch="Fill" />
</telerik:RadButton.Content>
</telerik:RadButton>

AvalonDock MVVM Anchorable Location

I have an AvalonDock (version 3.5) in my MVVM WPF application. The binding is as follows:
<xcad:DockingManager Name="_dockingManager" Grid.Row="1"
DataContext="{Binding DockingManagerViewModel}"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding Anchorables}" >
So when I add a new Anchorable to the corresponding "Anchorables" collection in my view model the corresponding view shows up. Nevertheless the views always show up docked on the right side of my application. How can I control that the view is docked on the left side of my application via the view model?
I don't think you can control this in you viewmodel.
There 2 ways for controlling this.
You can restore the layout from a previously saved (default) layout whenever the
application is started for te first time or
You can setup the XAML to use an initial layout as you wish (prefered solution)
For the second option:
You can use the XAML binding in the DockingManager class to implement your requirement:
See TestApp sample for full implementation of the below snippet (just change LeftSide to BottomSide to see the effect):
<avalonDock:DockingManager Grid.Row="1">
...
<avalonDock:LayoutRoot.LeftSide>
<avalonDock:LayoutAnchorSide>
<avalonDock:LayoutAnchorGroup>
<avalonDock:LayoutAnchorable Title="AutoHide1 Content" ContentId="AutoHide1Content" IconSource="/AvalonDock.TestApp;component/Images/address-book--pencil.png" >
<TextBox Text="{Binding TestTimer, Mode=OneWay, StringFormat='AutoHide Attached to Timer ->\{0\}'}"/>
</avalonDock:LayoutAnchorable>
<avalonDock:LayoutAnchorable Title="AutoHide2 Content" ContentId="AutoHide2Content">
<StackPanel Orientation="Vertical">
<TextBox/>
<TextBox/>
</StackPanel>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorGroup>
</avalonDock:LayoutAnchorSide>
</avalonDock:LayoutRoot.LeftSide>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
You can add a property (call it InitialPosition, or something like that) to your Anchorable view model, and implement a ILayoutUpdateStrategy to position the anchorable on the left, right or bottom side.
Add something like this to your XAML:
<xcad:DockingManager …>
…
<xcad:DockingManager.LayoutUpdateStrategy>
<local:LayoutUpdate />
</xcad:DockingManager.LayoutUpdateStrategy>
</xcad:DockingManager>
and your LayoutUpdate class:
class LayoutUpdate: ILayoutUpdateStrategy
{
static Dictionary<PaneLocation, string> _paneNames = new Dictionary<PaneLocation, string>
{
{ PaneLocation.Left, "LeftPane" },
{ PaneLocation.Right, "RightPane" },
{ PaneLocation.Bottom, "BottomPane" },
};
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
if (anchorableToShow.Content is IAnchorable anch)
{
var initLocation = anch.InitialLocation;
string paneName = _paneNames[initLocation];
var anchPane = layout.Descendents()
.OfType<LayoutAnchorablePane>()
.FirstOrDefault(d => d.Name == paneName);
if (anchPane == null)
{
anchPane = CreateAnchorablePane(layout, Orientation.Horizontal, initLocation);
}
anchPane.Children.Add(anchorableToShow);
return true;
}
return false;
}
static LayoutAnchorablePane CreateAnchorablePane(LayoutRoot layout, Orientation orientation,
PaneLocation initLocation)
{
var parent = layout.Descendents().OfType<LayoutPanel>().First(d => d.Orientation == orientation);
string paneName = _paneNames[initLocation];
var toolsPane = new LayoutAnchorablePane { Name = paneName };
if (initLocation == PaneLocation.Left)
parent.InsertChildAt(0, toolsPane);
else
parent.Children.Add(toolsPane);
return toolsPane;
}
public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorable)
{
// here set the initial dimensions (DockWidth or DockHeight, depending on location) of your anchorable
}
This code is extracted and changed a bit from the working application, with different types and names. It should probably work, but there might be a typo or other error somewhere.

Instantiating a new UserControl programmatically while setting a DataContext to facilitate binding

Starting with the MahApps-Material mashup demo, I'm trying to use a button click event to create a new TabItem from my Views. For now, the CustomTabItem will show text bound to some property from a FancyObject (being served to the View from my FancyTabViewModel). But I've got the DataContext, dependency property or the Binding done wrong.
public void NewTabOnClick(object sender, RoutedEventArgs e)
{
// create my new object from Models
FancyObject fo = new FancyObject();
// create the INofify VM and pass it my object
// the VM has a public VMFancyObject property to serve the fo
FancyTabViewModel fvm = new FancyTabViewModel(fo);
// create the new UserControl and set its context to the VM
CustomTabItem newTab = new CustomTabItem() {
Header = "New tab"
};
newTab.DataContext = fvm;
MainWindowTabs.Items.Add(newTab);
}
And in my <TabItem x:Class="MyProject.Views.CustomTabItem" there is a label so bound: <Label Content="{Binding VMFancyObject.SomeList.Count}"/>
I expect to see the default count of the List created in the FancyObject's constructor. However, after the new tab is created and added to the dragablz:TabablzControl, I just see a blank label.
I also tried <Label DataContext="{Binding Path=DataContext.FancyTabViewModel,RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}" Content="{Binding VMFancyObject.SomeList.Count}" HorizontalAlignment="Left" Margin="341,196,0,0" Foreground="Black" Background="#FF97FF02"/>

Handling Navigation in MVVM WPF application

I am developing a WPF application that follows MVVM. Now I am handling navigation of views in the following manner.
MainWindow View
<Border>
<StackPanel>
<local:Home
Content="{Binding CurrentView,Converter={StaticResource ViewConverterHome}, UpdateSourceTrigger=PropertyChanged}"/>
<local:Page1
Content="{Binding CurrentView,Converter={StaticResource ViewConverterPage1}, UpdateSourceTrigger=PropertyChanged}"/>
<local:Page2
Content="{Binding CurrentView,Converter={StaticResource ViewConverterPage2}, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</Border>
Home, Page1,Page2 are 3 views. HomeVM,Page1VM,Page2VM are view models corresponding to the views. There is a class call ApplicationViewModel that contains a property CurrentView of type CViewModelBase which is the parent class for all three viewmodels. ApplicationViewModel handles the navigation in the folowing manner
private void OnUserInputNextClicked(object sender, OperationInformationChangedEventArgs e)
{
do
{
if (this.CurrentView is HomeVM)
{
this.CurrentView = null;
Page1VM page1 = new Page1VM("BNM", "MATH HONS", "13");
page1.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
page1.BackCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputBackClicked);
this.CurrentView = page1;
break;
}
if (this.CurrentView is Page1VM)
{
this.CurrentView = null;
Page2VM page2 = new Page2VM("Kolkata", "Monoj", "Itachuna");
page2.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
page2.BackCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputBackClicked);
this.CurrentView = page2;
break;
}
if (this.CurrentView is Page2VM)
{
this.CurrentView = null;
HomeVM home = new HomeVM("Anirban", "30");
home.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
this.CurrentView = home;
break;
}
} while (false);
}
The navigation is working perfectly; But dispose of disappeared views are not getting called.So all the views live till the end. Is there any way to prevent that?
Your Views will always exist because you added a copy of each one to your UI with the XAML, even if the Content contained in them may not exist
Typically I will use a ContentControl to display content instead of creating an instance of the control for each content type, and I'll use DataTemplates to tell WPF how to draw each type of content.
For example,
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeVM}">
<local:Home Content="{Binding }" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Page1VM}">
<local:Page1 Content="{Binding }" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Page2VM}">
<local:Page2 Content="{Binding }" />
</DataTemplate>
</Window.Resources>
<Border>
<StackPanel>
<ContentControl Content="{Binding CurrentView}" />
</StackPanel>
</Border>
This way, you only have one instance of your Content in the VisualTree, and the DataTemplate WPF users to draw your content changes based on it's DataType.
I have an example of this kind of navigation with WPF on my blog if you're interested in checking out a full code sample
You need to change DataContext of MainWindow. It depends on your integration. When I make a MVVM application what I do is that pass MainWindow object to every view constructor. And whenever I have to move to next page (like on next button) I change the MainWindow object DataContext to new view.
Something like this.
public PageOneViewModel
{
private MainWindow _mainWindow;
public PageOneViewModel(MainWindow mainWindow)
{
// Here I am saving MainWindow object.
_mainWindow = mainWindow;
}
public OnNext()
{
// Here I am changing the view.
MainWindow.DataContext = new PageTwoViewModel(_mainWindow);
}
}
Have you considered using Frame?
<Frame Name="YourFrame" Navigated="OnNavigated"/>
and then you can call
YourFrame.CanGoBack(), YourFrame.GoBack()
etc.
Here's a link to my answer to a similar question with working source code. The technique I used is a little similar to Faisal's solution.
If you need a good downloadable sample solution that demonstrates navigation using a side menu, look at here and here(simpler example).

Categories