I have a small example github repo in which I like to open a custom ContentDialog (SpeechDialog) after a button is clicked, using the MVVMCross framework.
If I implement a ContentDialog with MVVM without a framework, the MainView will look like this:
public sealed partial class MainView : Page
{
public MainView()
{
this.InitializeComponent();
ISpeechDialogService dialog = new SpeechDialogService();
MainViewModel= new MainViewModel(dialog);
}
public MainViewModel MainViewModel{ get; set; }
}
But in MVVMCross I have an attributed MainView and I don't know how to pass the ContentDialog:
[MvxViewFor(typeof(MainViewModel))]
public sealed partial class MainView : MvxWindowsPage
{
public MainView()
{
InitializeComponent();
}
}
Some code for a better understanding:
SpeechDialogService.cs:
public class SpeechDialogService : ISpeechDialogService
{
public async Task ShowAsync()
{
var contentDialog = new Speech();
await contentDialog.ShowAsync();
}
}
directlink to the Speech.xaml
TL;DR
Is my approach right? If yes, how can I pass a ContentDialog to the MainViewModel? If not, how to implement a ContentDialog with MVVMCross?
I think you can use ViewModelLocator here and use MVVM pattern, regardless of the framework. See sample implementation.
public class ViewModelLocator
{
public MainPageViewModel MainPageViewModel
{
get
{
ISpeechDialogService dialogService = new SpeechDialogService();
MainPageViewModel vm = new MainPageViewModel(dialogService);
return vm;
}
}
}
Here, you can use Autofac to resolve your ViewModels' dependecies, also make the service singleton https://autofaccn.readthedocs.io/en/latest/resolve/index.html
Then in your App.xaml, add a resource for locator:
<Application.Resources>
<services:ViewModelLocator x:Key="Locator" />
</Application.Resources>
Then in your page (preferably not in code behind) you should assign your DataContext like so:
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DesignHeight="450" d:DesignWidth="800"
DataContext="{Binding MainPageViewModel, Source={StaticResource Locator}}">
</Page>
Then your ViewModel should look like:
using App1.Services;
namespace App1.ViewModels
{
public class MainPageViewModel
{
private readonly ISpeechDialogService _speechDialogService;
public MainPageViewModel(ISpeechDialogService speechDialogService)
{
_speechDialogService = speechDialogService;
}
}
}
Your dialog service look like:
using System.Threading.Tasks;
namespace App1.Services
{
public class SpeechDialogService : ISpeechDialogService
{
public async Task ShowAsync()
{
var contentDialog = new Speech();
await contentDialog.ShowAsync();
}
}
}
Hope this helps.
Related
I need to open a dialog window when I open a module.
In my module I register WindowA and I want to show it in method OnInitialize() of the module. It looks like this.
public class TestModule : IModule
{
IDialogWindow _dialogWindow;
public TestModule(IContainerProvider containerProvider, IDialogWindow dialogWindow)
{
_dialogWindow = dialogWindow;
}
public void OnInitialized(IContainerProvider containerProvider)
{
_dialogWindow.Show();
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialog<WindowA>();
}
}
My window
<Window x:Class="FirstModule.Views.WindowAView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
prism:ViewModelLocator.AutoWireViewModel="True"
xmlns:local="clr-namespace:FirstModule.Views"
mc:Ignorable="d"
Title="WindowA" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Content="Button"/>
</Grid>
</Window>
and View model for this
class WindowAViewModel : IDialogAware
{
public WindowAViewModel()
{
}
public string Title { get; }
public event Action<IDialogResult> RequestClose;
public bool CanCloseDialog()
{
return true;
}
public void OnDialogClosed()
{
}
public void OnDialogOpened(IDialogParameters parameters)
{
}
}
Also I implemented IDialogWindow
public partial class WindowAView : Window, IDialogWindow
{
public WindowAView()
{
InitializeComponent();
}
public IDialogResult Result { get; set; }
}
But instead of WindowAView a small window is shown with Width and Height equal to 0.
Could you please explain what I did wrong?
I think you misunderstood the dialog service, from the documentation:
Your dialog view is a simple UserControl that can be designed anyway you please. The only requirement it has a ViewModel that implements IDialogAware set as it's DataContext.
The dialog view is a UserControl that is shown in a dialog host. Prism uses a standard WPF window as default dialog host, but you can create your own by implementing the IDialogWindow interface and registering it. In Prism 7, there was only one dialog host for all. Since Prism 8, you can use the same dialog host for all dialog views or different dialog hosts as you want.
It's very common to be using a third-party control vendor such as Infragistics. In these cases, you may want to replace the standard WPF Window control that hosts the dialogs with a custom Window class such as the Infragistics XamRibbonWindow control.
In this case, just create your custom Window, and implement the IDialogWindow interface: [...] Then register your dialog window with the IContainerRegistry. [...] If you have more than one dialog window you would like to use as a dialog host, you register multiple dialog windows with the container by specifying a name for the window.
You inject the IDialogWindow into the module constructor, which is not your WindowAView, but simply the default dialog host window, which is empty, as there is no content to host. What you should do instead is define your view as a UserControl and adapt your WindowAViewModel accordingly.
<UserControl x:Class="FirstModule.Views.AView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
prism:ViewModelLocator.AutoWireViewModel="True"
xmlns:local="clr-namespace:FirstModule.Views"
mc:Ignorable="d"
Height="450"
Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Content="Button"/>
</Grid>
</UserControl>
public partial class AView : UserControl
{
public AView()
{
InitializeComponent();
}
}
class AViewModel : IDialogAware
{
public AViewModel()
{
}
public string Title { get; }
public event Action<IDialogResult> RequestClose;
public bool CanCloseDialog()
{
return true;
}
public void OnDialogClosed()
{
}
public void OnDialogOpened(IDialogParameters parameters)
{
}
}
Then register the dialog and use the IDialogService instead of IDialogWindow.
public class TestModule : IModule
{
IDialogService _dialogService;
public TestModule(IContainerProvider containerProvider, IDialogService dialogService)
{
_dialogService = dialogService;
}
public void OnInitialized(IContainerProvider containerProvider)
{
dialogService.Show(nameof(AView), new DialogParameters(), result => {});
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialog<AView>();
}
}
This will show your view in the default dialog window, but as stated above, you can always roll your own, if you need to. For more information, see the dialog service documentation.
All I have some experience with Caliburn.Micro using System.ComponentModel.Composition as an IoC container. This time I want to have some fun and use Niject. To setup the Calburn.Micro bootstrapper, I have the following class
public class Bootstrapper : BootstrapperBase
{
private IKernel _kernel;
public Bootstrapper()
{
Initialize();
}
protected override void Configure()
{
_kernel = new StandardKernel();
_kernel.Bind<IWindowManager>().To<WindowManager>().InSingletonScope();
_kernel.Bind<IEventAggregator>().To<EventAggregator>().InSingletonScope();
_kernel.Bind<IMainWindowViewModel>().To<MainWindowViewModel>().InSingletonScope();
}
protected override object GetInstance(Type service, string key)
{
return _kernel.Get(service);
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return _kernel.GetAll(service);
}
protected override void OnStartup(object sender, StartupEventArgs suea)
{
base.OnStartup(sender, suea);
DisplayRootViewFor<IMainWindowViewModel>();
}
protected override void OnExit(object sender, EventArgs e)
{
_kernel.Dispose();
base.OnExit(sender, e);
}
}
This seem to be called fine, but when the line
DisplayRootViewFor<IMainWindowViewModel>();
Is hit, it seems to launch the view IMainWindowView okay, but
public partial class MainWindowView : Window
{
public MainWindowView()
{
InitializeComponent();
}
}
public interface IMainWindowViewModel { }
and MainWindowViewModel as
public class MainWindowViewModel : Conductor<IMainWindowViewModel>, IMainWindowViewModel { }
with the XAML of IMainWindowView as
<Window x:Class="Mole.Replay.Framework.MainWindowView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:Mole.Replay.Framework"
xmlns:local="clr-namespace:Mole.Replay.Framework"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Window.DataContext>
<ViewModels:MainWindowView/>
</Window.DataContext>
<Grid>
</Grid>
</Window>
The ctor is called over and over again and results in a StackOverflow exception, there is no clear cause for this what-so-ever. This type is bound in singleton scope. Why is this happening am I missing something?
<Window.DataContext>
<ViewModels:MainWindowView/>
</Window.DataContext>
it doesn't make sense to set DataContext of MainWindowView to another instance of MainWindowView which will also try to set DataContext etc, until you get StackOverflow exception.
it should be a view model in DataContext. I don't know if caliburn.micro creates view models for view based on conventions, but at least remove current <Window.DataContext> assignment.
it is not clear to me why view namespace is aliased as ViewModels. If real view model is in the same namespace and it is not resolved automatically, assign view model
<Window.DataContext>
<ViewModels:MainWindowViewModel/>
</Window.DataContext>
DI container really should provide constructor arguments.Use them to assign DataContext:
public MainWindowView(IMainWindowViewModel vm)
{
InitializeComponent();
DataContext = vm;
}
I am trying to create a base class for my windows in a WPF application but I cannot access their members in the derived classes. For example, here is the base window:
namespace MyApp.Windows
{
public class BaseWindow : Window
{
public int MyProp { get; set; }
}
}
And here is a window:
public partial class SomeWindow : BaseWindow
{
public SomeWindow()
{
InitializeComponent();
Loaded += SomeWindow_Loaded;
}
private void SomeWindow_Loaded(object sender, RoutedEventArgs e)
{
MyProp = do something;
}
}
If I leave it like this, the MyProp proerty works just fine but I get an error that InitializeComponent() is not recognized. Therefore, in the window xaml I change the x:Class as follows:
Before
<Window x:Class="MyApp.SomeWindow"
After
<Window x:Class="MyApp.Windows.BaseWindow"
Now, InitializeComponent() doesn't give me anymore issues but MyProp suddenly isn't recognized. Why?
In case it helps, what I want is to have all windows raise an event once they are loaded (the Loaded event is fired) and I don't want to write this code for every window I have, so I thought I could write this code in a base class, derive my windows from this and, well, have everything work.
Edit: Here's all the code.
BaseWindow.cs (no other xaml):
using System.Windows;
namespace MyApp.Windows
{
public class BaseWindow : Window
{
public int MyProp { get; set; }
}
}
MainWindow.xaml.cs
namespace MyApp.Windows
{
public partial class MainWindow : BaseWindow
{
public MainWindow()
{
InitializeComponent();
}
}
}
MainWindow.xaml:
<myapp:BaseWindow x:Class="MyApp.Windows.BaseWindow"
xmlns:myapp="clr-namespace:MyApp.Windows"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</myapp:BaseWindow>
In order to change the base class of SomeWindow from Window to BaseWindow, you need to replace Window by BaseWindow wherever it occurs.
So
public partial class SomeWindow : Window
becomes
public partial class SomeWindow : BaseWindow
and
<Window x:Class="MyApp.Windows.SomeWindow" ...>
becomes
<myapp:BaseWindow x:Class="MyApp.Windows.SomeWindow"
xmlns:myapp="clr-namespace:MyApp.Windows" ...>
with the unavoidable XAML namespace prefix.
This is the BaseWindow class used in the example above:
namespace MyApp.Windows
{
public class BaseWindow : Window
{
public int MyProp { get; set; }
public BaseWindow()
{
Loaded += BaseWindow_Loaded;
}
private void BaseWindow_Loaded(object sender, RoutedEventArgs e)
{
}
}
}
How can you create a binding between a ViewModel and a View?
In the past there was a Locater created in App.xaml and then on the view you had this:
DataContext="{Binding MainViewModel, Source={StaticResource ViewModelLLocator}}"
I can't even click in the Properties of the View and then create DataContext binding.
In recent versions of MVVM light they changed how ViewModelLocator works due to it taking a dependency on Microsoft.Practices.ServiceLocation and the former not being .NET Standard compliant. It now should use GalaSoft.MvvmLight.Ioc to locate the ViewModel using SimpleIoc.
Here's an example how I used it in a recent UWP project.
In App.xaml
private ViewModels.ViewModelLocator Locator => Application.Current.Resources["Locator"] as ViewModels.ViewModelLocator;
In MainPage.xaml
DataContext="{Binding MainViewModel, Source={StaticResource Locator}}">
In MainPage.cs
private MainViewModel ViewModel
{
get { return DataContext as MainViewModel; }
}
In ViewModelLocator.cs
namespace YourNamespace.ViewModels
{
public class ViewModelLocator
{
public ViewModelLocator()
{
Register<MainViewModel, MainPage>();
}
public MainViewModel MainViewModel => SimpleIoc.Default.GetInstance<MainViewModel>();
public void Register<VM, V>()
where VM : class
{
SimpleIoc.Default.Register<VM>();
NavigationService.Configure(typeof(VM).FullName, typeof(V));
}
}
}
Ok I found it out:
You need to add this in App.xaml:
private static ViewModelLocator _locator;
public static ViewModelLocator Locator => _locator ?? (_locator = new ViewModelLocator());
And then in the View.xaml:
this.DataContext = App.Locator.MainViewModel;
MainWindow UI
<Window x:Class="PrismSanity.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock Text="{Binding MyName}"></TextBlock>
</Grid>
</Window>
MainWindowViewModel
public class MainWindowViewModel : BindableBase
{
private string _MyName = "John";
public string MyName
{
get { return _MyName = "John"; }
set { SetProperty(ref _MyName, value); }
}
}
I installed SNOOP to see what was happening, but the VM is not being Linked to the View. If I do the same thing with a second view, and use
<ContectContainer>
<views:ViewA/>
</ContentContainer>
in the Mainwindow
Then I get the ViewA and ViewAViewModel to link fine.
Thanks for looking.
Prism supports only UserControl class not Window class. AutoWireViewModel wokrs only with UserControl class.
I have the same problem I resolve this binding the context in code behind - partial using Unity. Something like that:
public partial class ShellView : Window
{
public ShellView(ShellViewModel viewModel)
{
this.InitializeComponent();
// Set the ViewModel as this View's data context.
this.DataContext = viewModel;
}
When app create the ShellView (it is your PrismSanity.MainWindow) unity inject the viewModel (ShellViewModel )
It is little technology debt.