Good afternoon everyone. I have just begun to work with Xamarin Forms as I will need it for my job and wanted to learn best practices from the beginning so I went for Mvvm Light. My current problem is that, whatever I do, I really can't make binding work (NOTE: it worked for my first project, a simple page where I had a button which was used to navigate to another page). Now, for my current project, an EventApp I tried everything I could, looking on lots of forums and tried various settings and implementations but couldn't make the binding work again. I will show you some of my current code for a test page I've done just for testing purposes.
App.cs class:
public partial class App : Application
{
private static ViewModelLocator _locator;
public static ViewModelLocator Locator { get { return _locator ?? (_locator = new ViewModelLocator()); } }
public App()
{
InitializeComponent() //Tried both with and without this
registerNavigationService();
}
protected override void OnStart()
{
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
public void registerNavigationService()
{
NavigationService nav = new NavigationService();
nav.Configure(PageNames.GeneralNotificationPage, typeof(GeneralNotificationPage));
nav.Configure(PageNames.CreateNotificationPage, typeof(CreateNotificationPage));
nav.Configure(PageNames.SelectSongPage,typeof(SelectSongPage));
SimpleIoc.Default.Register<INavigationService>(() => nav);
var firstPage = new NavigationPage(new SelectSongPage());
nav.Initialize(firstPage);
MainPage = firstPage;
}
}
`
App.xaml:
<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="EventAppMvvm.App"
>
</Application>
-Note: here I tried to set a static resource for my ViewModelLocator the next way:
xlmns:vm="clr-namespace:EventAppMvvm.ViewModel;assembly=EventAppMvvm"
and then in resources
<vm:ViewModelLocator x:Key="Locator"/>
doesn't work, if I try in one of my pages to set the DataContext, it says it hasn't found any bindable property. If I set the BindingContext to that I get an NPE when I run the app as there is no object created. So I ended up using the code behind to do the BindingContext and instantiate the locator in my App.cs file so I won't receive an NPE.
ViewModelLocator:
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
////if (ViewModelBase.IsInDesignModeStatic)
////{
//// // Create design time view services and models
//// SimpleIoc.Default.Register<IDataService, DesignDataService>();
////}
////else
////{
//// // Create run time view services and models
//// SimpleIoc.Default.Register<IDataService, DataService>();
////}
SimpleIoc.Default.Register<CreateNotificationPageViewModel>();
SimpleIoc.Default.Register<SelectSongPageViewModel>();
SimpleIoc.Default.Register<GeneralNotificationPageViewModel>();
}
public CreateNotificationPageViewModel CreateNotificationPageVM
{
get
{
return ServiceLocator.Current.GetInstance<CreateNotificationPageViewModel>();
}
}
public SelectSongPageViewModel SelectSongPageVM
{
get
{
return ServiceLocator.Current.GetInstance<SelectSongPageViewModel>();
}
}
public GeneralNotificationPageViewModel GeneralNotificationPageVM
{
get
{
return ServiceLocator.Current.GetInstance<GeneralNotificationPageViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
One of my ViewModels(test one):
public class SelectSongPageViewModel : ViewModelBase
{
RelayCommand _commandTest;
Song _selectedSong;
INavigationService navigation;
string clicked = "3";
string _message = "I ve not been clicked yet";
public SelectSongPageViewModel(INavigationService navigation)
{
this.navigation = navigation;
}
RelayCommand CommandTest
{
get
{
if(_commandTest == null)
{
_commandTest= new RelayCommand(() =>
{
Message = _message;
//navigation.NavigateTo(PageNames.CreateEventPage);
});
}
return _commandTest;
}
}
public string Message
{
get
{
return clicked;
}
set
{
clicked = value;
RaisePropertyChanged("Message");
}
}
}
Actual page of that ViewModel:
Notes: if I try to use DataContext (defined as static resource previously in App.xaml), no matter if I give the correct namespace and assembly, it says that it hasn't found any binding, property there, if I use BindingContext, it works, I don't get any errors at compile time but I can't make the buttons do something as it is like it doesn't recognize my defined commands but for some reason it just binds simple data like my Message property.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="EventAppMvvm.Views.SelectSongPage"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
>
<ContentPage.Content>
<StackLayout>
<Label Text="{Binding Message}"></Label>
<Button Text="Press me" Command="{Binding CommandTest}"></Button>
</StackLayout>
</ContentPage.Content>
</ContentPage>
And the code behind for this page.
namespace EventAppMvvm.Views
{
public partial class SelectSongPage : ContentPage
{
public SelectSongPage()
{
InitializeComponent();
var vm = App.Locator.SelectSongPageVM;
BindingContext = vm;
}
}
}
Whatever I do, I can't make the commands/Complex binding work again for some reason but still simple property binding works, I'm using VisualStudio for Mac if it matters.
Related
I am new to .Net Maui. I have follow James Montemagno's video tuorial:
https://www.youtube.com/watch?v=ddmZ6k1GIkM&list=PLdo4fOcmZ0oUBAdL2NwBpDs32zwGqb9DY&index=6 on navigating between pages. I can successfully navigate from one page to the next and also send a complex data object as a parameter. However, on the new page, I want to use the parameter to fetch objects via a webservice in my ViewModel, and bind the xaml to the newly fetched objects.
At first I tried this in the ViewModels' constructor, but the parameters where still null inside the constructor. I then invoked an event that fired when the property I am binding to changes partial void OnLoginResultChanged(LoginResult value). From there I call an async Task to fetch the objects from the webservice and try and bind and display them in the xaml. Unfortunately, I am not getting anything to show in the xaml.
Here is my code:
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MealPacApp.Model;
namespace MealPacApp.ViewModel
{
[QueryProperty(nameof(Model.LoginResult), "LoginResult")]
public partial class MainViewModel : ObservableObject
{
IConnectivity connectivity;
Services.MealPacService mealPacService;
[ObservableProperty]
Model.User user = new();
[ObservableProperty]
Model.LoginResult loginResult;
partial void OnLoginResultChanged(LoginResult value)
{
this.GetDataForViewCommand.ExecuteAsync(null);
}
public MainViewModel(IConnectivity connectivity, Services.MealPacService mealPacService)
{
this.connectivity = connectivity;
this.mealPacService = mealPacService;
//Now check to see if there is internet connectivity
if (connectivity.NetworkAccess != NetworkAccess.Internet)
{
Shell.Current.DisplayAlert("Oops!", "No internet is available. Please ensure you have an internet connection before continuing.", "OK");
return;
}
}
[RelayCommand]
async Task GetDataForView()
{
try
{
if (loginResult != null)
{
//Its not empty, so login here
var incmoingUser = await Services.MealPacService.GetUser(loginResult.Reference, loginResult.OrganisationId);
if (incmoingUser != null)
{
user = incmoingUser;
}
}
}
catch (Exception)
{
throw;
}
}
}
}
Here is the View (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"
x:Class="MealPacApp.MainPage"
Title="Home"
xmlns:viewmodel="clr-namespace:MealPacApp.ViewModel"
xmlns:model="clr-namespace:MealPacApp.Model"
x:DataType="viewmodel:MainViewModel">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Label
Text="{Binding User.Name}"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Here is the Model I am trying to bind too:
namespace MealPacApp.Model
{
public partial class User
{
public int Userid { get; set; }
public string Reference { get; set; }
public string Name { get; set; }
}
}
Here is my page code behind:
using MealPacApp.ViewModel;
namespace MealPacApp
{
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
}
As mentioned, the navigation is working, I am just missing how to get the property change to reflect in the Xaml. I have kept the [ObservableProperty] instead of implementing INotifyPropertyChanged as to my understanding, it takes care of the boiler plate stuff.
Thanks in advance.
This line
user = incmoingUser;
Is updating the private field user
You want to update the public property User which is observable
User = incmoingUser;
I'm making an app which has a a ZXing ScannerView in a ContentPage.
I've managed to make it read a QR code just fine in a function in my ScanningViewModel.
However, when I try to navigate away from the page with the ScannerView, it crashes.
In the 'Application Output' in Visual Studio, I'm seeing a load of the 'Too soon between frames' error, which is I believe is causing the crashing. I've read that setting delays to 5 might help, but I'm not sure how to do this. This is where I read this: https://github.com/Redth/ZXing.Net.Mobile/issues/721
I've also seen some other StackOverflow articles, but they didn't really answer my question.
Is there a way to fix this?
Edit: This is the other post I read on StackOverflow:
Zxing Mobile doesn't stop analysing on iOS
XAML Page:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
xmlns:viewmodel1="clr-namespace:DoorRelease.ViewModel"
xmlns:viewmodel="clr-namespace:GardisMobileApp.ViewModel"
x:Class="GardisMobileApp.QRScanningPage">
<ContentPage.BindingContext>
<viewmodel:ScanningViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<StackLayout>
<Label Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
<zxing:ZXingScannerView x:Name="scanner" IsScanning="{Binding isScanning}" ScanResultCommand="{Binding GetResultCommand}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
My Code Behind:
namespace MobileApp
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class QRScanningPage : ContentPage
{
public QRScanningPage()
{
InitializeComponent();
}
}
}
My ScanningViewModel:
namespace MobileApp.ViewModel
{
public class ScanningViewModel : BaseViewModel
{
private static ScanningViewModel _instance = new ScanningViewModel();
public static ScanningViewModel Instance { get { return _instance; } }
public string stsAddress { get; set; }
public string apiAddress { get; set; }
public bool isScanning { get; set; } = true;
public Command GetResultCommand { get; set; }
public ScanningViewModel() : base()
{
Title = "QR Code Scanner";
GetResultCommand = new Command(async(r) => await GetScannedAsync(r));
}
async Task GetScannedAsync(object result)
{
isScanning = false;
try
{
var resultArray = result.ToString().Split(',');
stsAddress = resultArray[0];
apiAddress = resultArray[1];
MainThread.BeginInvokeOnMainThread(async () =>
{
await Application.Current.MainPage.Navigation.PopAsync();
//await Application.Current.MainPage.DisplayAlert("Code scanned", "You've scanned a QR code!", "OK");
});
}
catch(Exception e)
{
await Application.Current.MainPage.DisplayAlert("Error!", e.Message, "OK");
}
}
}
}
From document INotifyPropertyChanged Interface,we know that
The INotifyPropertyChanged interface is used to notify clients, typically binding clients, that a property value has changed.
For change notification to occur in a binding between a bound client and a data source, your bound type should either:
Implement the INotifyPropertyChanged interface (preferred).
Provide a change event for each property of the bound type.
Do not do both.
In your code, if you want to update the UI while changing the value of isScanning , you have to inplement interface INotifyPropertyChanged .
public bool isScanning { get; set; } = true;
Please refer to the following code:
private bool _isScanning;
public bool isScanning
{
get
{
return _isScanning;
}
set
{
SetProperty(ref _isScanning, value);
}
}
And assign an initial value (true) for it in the constructor of class ScanningViewModel:
public ScanningViewModel()
{
//assign an initial value (`true`)
isScanning = true;
// other code
}
I'm trying to open a new Page on a tap event, which should display some detailed information on a certain object. For that to work, I need to pass the object itself or its ID to the new page. So I added an argument to the constructor of the detail page as follows.
void onItemTapped(object sender, ItemTappedEventArgs e)
{
if (e.Item != null)
{
bool convOk = Int32.TryParse((string)e.Item, out int id);
if (convOk)
{
Navigation.PushAsync(new DetailPage(id));
}
}
}
And the DetailPage has its own DetailViewModel, which is set as the BindingContext within the code-behind.
DetailPage XAML:
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Foo.Views.DetailPage">
<ContentPage.Content>
<StackLayout Orientation="Vertical">
<Label Text="FooBar" />
<Label Text="{Binding trackID}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
DetailPage code-behind
namespace Foo.Views
{
public partial class DetailPage : ContentPage
{
public DetailPage(int trackID)
{
InitializeComponent();
BindingContext = new DetailViewModel(trackID);
}
}
}
DetailViewModel:
namespace Foo.ViewModels
{
public class DetailViewModel : BaseViewModel
{
// trackID prop
int _trackID;
int trackID
{
get { return _trackID; }
set
{
_trackID = value;
notifyPropertyChanged(nameof(trackID));
}
}
public TargetDetailViewModel(int tid)
{
trackID = tid;
}
}
}
However, the binding between the DetailPage and the DetailViewModel doesn't seem to work, the page doesn't show anything. The id itself is passed correctly all the way down to the DetailViewModel.
Is this due to the order of initialization? I presume that everything written in XAML will be executed in the DetailPage.InitializeComponent() method? If that's correct, is it safe/correct to instantiate the ViewModel before the DetailPage.InitializeComponent()?
Any hint appreciated.
Your trackID property is not public.
Note: if you watch the application log output you can catch binding problems like this (filter it by the string Binding:)
Log example of a private variable not being bound:
Binding: 'trackID' property not found on 'XXXX.VM', target property: 'Xamarin.Forms.Label.Text'
DetailViewModel Fix:
public class DetailViewModel : BaseViewModel
{
int _trackID;
public int trackID;
{
get { return _trackID; }
set
{
_trackID = value;
notifyPropertyChanged(nameof(trackID));
}
}
~~~~
}
I know that this exact question has been asked hundreds of time, but every user has a different problem and I have already tried many solutions, not getting anywhere.
The good thing is that banner test ads (like the one here: LINK) are showing correctly… and I have registered a Unit banner in AdMob (without connecting FireBase, yet)
This is my code, taken from various samples in the internet… it's a pretty standard code pattern for everyone of them:
MainPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TestAd"
xmlns:localIOS="clr-namespace:TestAd.iOS"
xmlns:vm="clr-namespace:TestAd.ViewModels"
x:Class="TestAd.MainPage">
<ContentPage.BindingContext>
<vm:AppViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Grid …>
</Grid>
<localIOS:AdMobView AdUnitId="ca-app-pub-XXXXXXXXXXXXXXX/XXXXXXXXXX"/>
<ListView ...>
</ListView>
</StackLayout>
AdView
public class AdMobView : View
{
public static readonly BindableProperty AdUnitIdProperty = BindableProperty.Create(
nameof(AdUnitId),
typeof(string),
typeof(AdMobView),
string.Empty);
public string AdUnitId
{
get => (string)GetValue(AdUnitIdProperty);
set => SetValue(AdUnitIdProperty, value);
}
}
AdViewRenderer
[assembly: ExportRenderer(typeof(AdMobView), typeof(AdMobViewRenderer))]
namespace AppReminderIOS.iOS
{
public class AdMobViewRenderer : ViewRenderer<AdMobView, BannerView>
{
protected override void OnElementChanged(ElementChangedEventArgs<AdMobView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
SetNativeControl(CreateBannerView());
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(BannerView.AdUnitID))
Control.AdUnitID = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXX"; //Even tried: "Element.AdUnitId;"
}
private BannerView CreateBannerView()
{
var bannerView = new BannerView(AdSizeCons.SmartBannerPortrait)
{
AdUnitID = "ca-app-pub-XXXXXXXXXXXXXXXXXX/XXXXXXXXX", //Even tried: "Element.AdUnitId,"
RootViewController = GetVisibleViewController()
};
bannerView.LoadRequest(GetRequest());
Request GetRequest()
{
var request = Request.GetDefaultRequest();
return request;
}
return bannerView;
}
private UIViewController GetVisibleViewController()
{
var windows = UIApplication.SharedApplication.Windows;
foreach (var window in windows)
{
if (window.RootViewController != null)
{
return window.RootViewController;
}
}
return null;
}
}
}
AppDelegate.cs
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Google.MobileAds.MobileAds.Configure("ca-app-pub-XXXXXXXXXXXXXXXXXX~XXXXXXXX");
Xamarin.Forms.Forms.Init();
LoadApplication(new App());
return base.FinishedLaunching(app, options);
}
}
This is the main code… what do you think?
Could there be a problem with my AdMob account?
Test ads are showing correctly AND they even refresh thereselves as I rotate the device… so I don't know what to think.
Thank you for you kindness in helping me.
If you have recently created an AD unit ID(in 24 hours), it may take some time and several AD requests to build up AD resources. Because of this, you may not immediately see the actual presentation. You should see more consistent results when your application requests multiple times. Please note that the test AD runs through the same channels as the actual AD. If the test AD returns, your application is communicating correctly with the network.
EDIT: Added concrete example to clarify what I trying to achieve.
Here is application scheme:
To make code simpler, I will use trivial Messenger class instead of event aggregator from Prism. Tuple contains Id and string payload.
public static class Messenger
{
public static event EventHandler<Tuple<int, string>> DoWork;
public static void RaiseDoWork(int id, string path)
{
DoWork?.Invoke(null, new Tuple<int, string>(id, path));
}
}
Model instance subscribe to messenger for knowing when to start work (if Id correct), and notify view-model when work finished.
public class Model
{
public int id;
public Model(int id)
{
this.id = id;
Messenger.DoWork += (sender, tuple) =>
{
if (tuple.Item1 != this.Id)
{
return;
}
var result = tuple.Item2 + " processed with id " + this.id;
this.OnWorkCompleted(result);
};
}
public event EventHandler<string> WorkCompleted;
private void OnWorkCompleted(string path)
{
this.WorkCompleted?.Invoke(null, path);
}
}
UserControlResult is responsible for payload processing and result output. To make code simpler, lets just trace output instead of putting it on UI. So XAML will be default.
Code-behind:
public partial class UserControlResult : UserControl
{
private ResultViewModel viewModel;
public UserControlResult()
{
this.InitializeComponent();
}
public void Init(int id)
{
this.viewModel = new ResultViewModel(id);
this.DataContext = this.viewModel;
}
}
View-model:
public class ResultViewModel
{
private Model model;
public ResultViewModel(int id)
{
this.model = new Model(id);
this.model.WorkCompleted += path =>
{
Trace.WriteLine(path);
};
}
}
UserControlButtons contains buttons, one of them should start processing of model in UserControlResult via messenger. To make code simpler, lets omit command implementation and just show its handler.
Code-behind:
public partial class UserControlButtons : UserControl
{
private ButtonsViewModel viewModel;
public UserControlButtons()
{
this.InitializeComponent();
}
public void Init(int id)
{
this.viewModel = new ButtonsViewModel(id);
this.DataContext = this.viewModel;
}
}
View-model:
public class ButtonsViewModel
{
private int id;
public ButtonsViewModel(int id)
{
this.id = id;
}
// DelegateCommand implementation...
private void StartWorkingCommandHandler()
{
Messenger.RaiseDoWork(this.id, "test path");
}
}
UserControlParent contains both UserControlResult and UserControlButtons. His only role is to pass Id to them, so he doesn't even need view-model.
Xaml:
<StackPanel>
<uc:UserControlResult x:Name="UserControlResult" />
<uc:UserControlButtons x:Name="UserControlButtons" />
</StackPanel>
Code-behind:
public partial class UserControlParent : UserControl
{
public UserControlParent()
{
this.InitializeComponent();
}
public void Init(int id)
{
this.UserControlResult.Init(id);
this.UserControlButtons.Init(id);
}
}
And finally MainWindow contains two instances of UserControlParent. Its role to assign them different Ids.
Xaml:
<StackPanel>
<uc:UserControlParent x:Name="UserControlParent1" />
<uc:UserControlParent x:Name="UserControlParent2" />
</StackPanel>
Code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.UserControlParent1.Init(111);
this.UserControlParent2.Init(222);
}
}
This will work: pressing button in UserControlButtons will start working in UserControlResult model, and both UserControlParent will working correct and independend thanks to Id.
But I believe that this chain of invoking Init methods is violates MVVM because code-behind (which is View in MVVM) should not know anything about Id value (which is relative to Model in MVVM). Talking that, I'm sure that Id is not part of view-model, because it doesn't have any presentation in UI.
How can I pass Id value from top window to "deepest" view-models without violating MVVM?
Original Question
Here is WPF application consisting from 3 UserControls:
UserControl3 is a part of UserControl2 content. I keep MVVM during developing and using Prism.
I need to invoke method of custom class (which is model in terms of MVVM) in UserControl3 from view-model of UserControl1. The restriction that custom class can't be singleton. I suppose to do it one of the following way:
Using event aggregator from Prism. UserControl1 view-model is publisher and UserControl3 model is subscriber. For this I'll need to create unique Id in Window and pass it to UserControl1 and UserControl3.
Creating service instance in Window and pass it to UserControl1 and UserControl3. Then UserControl1 will just invoke method of this instance.
Window pass UserControl2 instance to UserControl1. View-model in UserControl1 will just invoke method of UserControl2, which will invoke method of UserControl3 and so on.
It seems like 2 and 3 approaches violates MVVM. How would you resolve this situation?
I would use option 1. I use MVVM Light to send a message and whoever receives that specific message will fire off the service method. Loosely coupled.
I think I achieved truly MVVM implementation shown in simplified example below. Special thanks to Ed Plunkett's comment and Nikita's answer.
First, I don't need to pass unique Ids anymore. For identification of different ParentViewModel instances, I just pass them different Messenger instances (which replaces Prism's EventAggregator for the sake of simplicity):
internal class Messenger
{
public event EventHandler<string> DoWork;
public void RaiseDoWork(string path)
{
this.DoWork?.Invoke(this, path);
}
}
Second, it seems like in my particular case Model should not worry about Messenger's DoWork event. As soon as this event raised in one view-model (ButtonsViewModel), it is more appropriate for this event to be consumed by another view-model (ResultViewModel) rather than by Model itself. So Model simplified too:
internal class Model
{
public string Process(string input)
{
return input + " processed!";
}
}
Below demonstrated all view-models "from top to bottom".
internal class MainViewModel
{
private readonly Messenger eventAggregator1 = new Messenger();
private readonly Messenger eventAggregator2 = new Messenger();
public MainViewModel()
{
this.ParentViewModel1 = new ParentViewModel(this.eventAggregator1);
this.ParentViewModel2 = new ParentViewModel(this.eventAggregator2);
}
public ParentViewModel ParentViewModel1 { get; }
public ParentViewModel ParentViewModel2 { get; }
}
internal class ParentViewModel
{
public ParentViewModel(Messenger eventAggregator)
{
this.ButtonsViewModel = new ButtonsViewModel(eventAggregator);
this.ResultViewModel = new ResultViewModel(eventAggregator);
}
public ButtonsViewModel ButtonsViewModel { get; }
public ResultViewModel ResultViewModel { get; }
}
internal class ButtonsViewModel
{
private readonly Messenger eventAggregator;
public ButtonsViewModel(Messenger eventAggregator)
{
this.eventAggregator = eventAggregator;
this.StartCommand = new DelegateCommand(this.StartProcessing);
}
public DelegateCommand StartCommand { get; }
private void StartProcessing()
{
this.eventAggregator.RaiseDoWork("test path");
}
}
internal class ResultViewModel : ViewModelBase
{
private readonly Model model = new Model();
private string textValue;
public ResultViewModel(Messenger eventAggregator)
{
eventAggregator.DoWork += (sender, s) => this.DoWorkHandler(s);
}
public string TextValue
{
get { return this.textValue; }
set { this.SetProperty(ref this.textValue, value); }
}
private void DoWorkHandler(string s)
{
var result = this.model.Process(s);
this.TextValue = result;
}
}
Note that in ResultViewModel I replaced Trace.WriteLine with actual screen output (because now strings are without Id, so trace output the same). ViewModelBase just implements INotifyPropertyChanged.
Below demonstrated content part of all views "from top to bottom".
<!-- MainWindow.xaml -->
<StackPanel Orientation="Horizontal">
<views:UserControlParent DataContext="{Binding ParentViewModel1}" />
<views:UserControlParent DataContext="{Binding ParentViewModel2}" />
</StackPanel>
<!-- UserControlParent.xaml -->
<StackPanel>
<local:UserControlResult DataContext="{Binding ResultViewModel}" />
<local:UserControlButtons DataContext="{Binding ButtonsViewModel}" />
</StackPanel>
<!-- UserControlButtons.xaml -->
<Grid>
<Button Content="Test" Command="{Binding StartCommand}" />
</Grid>
<!-- UserControlResult.xaml -->
<Grid>
<TextBlock Text="{Binding TextValue}" />
</Grid>
And finally this two worlds are connected in App.xaml.cs:
private void App_OnStartup(object sender, StartupEventArgs e)
{
new MainWindow { DataContext = new MainViewModel() }.Show();
}
Seems like MVVM, but any remarks are welcome.