Finally I decided to jump on the WPF bandwagon and decided to follow the MVVM Pattern to create my applications. I am also using Caliburn.Micro.
I found many examples of Binding data to Windows but all the examples contained just one MainWindow. I couldn't figure out how to reference and make the binding when opening a second and third window. To illustrate my problem I created a simple application. This application has two windows, the main one named ShellView and the second one named Window1View. All I need in this application is to display the content of myStr1 into the TextBox on Window1View.
Here is the code:
Views.ShellView.xaml
<Window x:Class="Test.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock x:Name="Title" />
<Button Content="Window 1" Height="31" HorizontalAlignment="Left" Margin="24,268,0,0" Name="btnWin1" VerticalAlignment="Top" Width="87" Click="btnWin1_click" />
</Grid>
</Window>
Views.ShellView.xaml.cs
namespace Test.Views
{
using System.Windows;
public partial class ShellView : Window
{
public ShellView()
{
InitializeComponent();
}
private void btnWin1_click(object sender, RoutedEventArgs e)
{
Window1View win1 = new Window1View();
win1.Show();
}
}
}
ViewModels.ShellViewModel.cs
namespace Test.ViewModels
{
using Caliburn.Micro;
public class ShellViewModel : PropertyChangedBase
{
public static string txt1 = "String 1";
public static string txt2 = "String 2";
private string title;
public string Title
{
get { return title; }
set
{
if (title != value)
{
title = value;
RaisePropertyChangedEventImmediately("Title");
}
}
}
public ShellViewModel()
{
Title = "Hello Caliburn.Micro";
}
}
}
Views.Window1View.xaml
<Window x:Class="Test.Views.Window1View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
Title="Window 1" Height="300" Width="300">
<Grid>
<Label Content="TextBox 1" Height="26" HorizontalAlignment="Left" Margin="12,40,0,0" Name="label1" VerticalAlignment="Top" Width="75" />
<TextBox Height="29" HorizontalAlignment="Left" Margin="106,39,0,0" Name="txtBox1" VerticalAlignment="Top" Width="145" Text="{Binding myStr1}" />
</Grid>
</Window>
View.Window1View.xaml.cs
using System.Windows;
namespace Test.Views
{
/// <summary>
/// Interaction logic for Window1View.xaml
/// </summary>
public partial class Window1View : Window
{
public Window1View()
{
InitializeComponent();
}
}
}
ViewModels.Window1ViewModel.cs
namespace Test.ViewModels
{
class Window1ViewModel
{
public Window1ViewModel()
{
myStr1 = ShellViewModel.txt1;
}
public string myStr1 { get; set; }
}
}
Bootstrapper.cs
namespace Test
{
public class Bootstrapper : Caliburn.Micro.Bootstrapper<Test.ViewModels.ShellViewModel>
{
}
}
App.xaml
<Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Test.App">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.cs
namespace Test
{
using System.Windows;
public partial class App : Application
{
Bootstrapper bootstrapper;
public App()
{
bootstrapper = new Bootstrapper();
}
}
}
Any help with this would be greatly appreciated.
Thanks
Carmelo
Maybe I'm missing something because I'm not familiar with Caliburn.Micro, but I see a couple of things going on here.
First, I don't see anywhere that you're setting the DataContext of the Window1View to be an instance of Window1ViewModel.
Second, your Window1ViewModel doesn't derive from PropertyChangedBase and you don't call RaisePropertyChanged when you change the myStr1 property.
A common way to implement view model communication in MVVM is to use the Mediator Pattern. Most MVVM frameworks include a 'Messenger' class that allows you to decouple your design by publishing and subscribing to events. In Caliburn Micro, mediation is supported by the EventAggregator class.
Since you are new to MVVM, I would also recommend the following resources:
Implementing the MVVM Pattern
Advanced MVVM Scenarios
User Interaction Patterns
A few things here:
You are using code behind unnecessarily. Ideally you should aim for virtually no code behind when using MVVM. Instead of creating a btnWin1_click handler, name the button and implement a method on your view model with the same name. Caliburn.Micro will invoke the view model method based on convention.
Use x:Name rather than Name
When displaying the Window1ViewModel, you are not using Caliburn.Micro to invoke the window display. This means that no binding exists between your Window1View and Window1ViewModel. If this is a separate window, use the WindowManager type from Caliburn.Micro. Instantiate your Window1ViewModel, and use the WindowManager class to display it. Caliburn.Micro will locate the appropriate view based on conventions, and bind the view to your view model.
As mentioned, rather than reference the ShellViewModel directly in the Window1ViewModel (which couples the view models and makes Window1ViewModel less reuseable), use the mediator pattern. Caliburn.Micro comes with an EventAggregator class - you can publish the change in text from the ShellViewModel, and subscribe to the event in your Window1ViewModel.
Related
I come from a WPF background so I thought I'd experiment with building a to-do app in WinUI 3. The app structure is a little overdesigned as I'm trying build it out like a more complex app. For that reason I have a ToDoTaskView and ToDoTaskViewModel, along with a MainWindowView and MainWindowViewModel even though it'd be much easier to build the entire app in a single XAML file.
The ToDoTaskView has a delete button, but the delete command lives on the MainWindowViewModel, as that's where the list that it must be deleted from lives. I think this a pretty common pattern where a sub-view needs to send a command to a parent view model.
The (abridged) MainWindowView:
<UserControl>
<ItemsRepeater ItemsSource="{Binding Tasks}">
<DataTemplate>
<local:ToDoTaskView />
</DataTemplate>
</ItemsRepeater>
</UserControl>
And the (heavily abridged) ToDoTaskView:
<UserControl>
<Button Command="???">Delete</Button>
</UserControl>
In WPF there's many ways to deal with this.
RoutedCommand
My prefered method. The MainWindowView can listen for a custom ToDoTaskDeleted routed command and bind to the command on the view model. Then any UI element anywhere underneath MainWindowView can fire said event and rest easy knowing it'll be handled somewhere above it on the visual tree.
There's no RoutedCommand in WinUI 3, and even worse, routed events are locked down and you can't define custom ones. So even building a custom RoutedCommand implementation would be difficult.
DynamicResource
I can define a StandardUICommand in MainWindowView.Resources, bind it to the command in the view model, then in ToDoTaskView I can use {DynamicResource DeleteCommand} to have the resource system search up the visual tree for the command.
Except I can't. WinUI3 doesn't have DynamicResource, only StaticResource. And since the two views are in different XAML files, and ToDoTaskView in a templated context, StaticResource can't resolve the resource name between them.
I think this could work for resources in App.xaml, but I'd rather not shove every command into the top level scope instead of keeping them where they belong.
All the commanding examples in the Microsoft docs seem to assume that the button and handler are in the same file, or they directly pass a reference to the command through to the child view's DataContext.
RelativeAncestor
Peter below reminded me that I tried this too, and found it's missing in WinUI 3. RelativeSource doesn't support any kind of ancestor discovery.
Manual Kludge
Setting up a direct reference from ToDoTaskViewModel to MainWindowViewModel is certainly possible, but I hate it. After all, who's to guarantee that this particular to do item is part of a list at any one moment? Maybe it lives in a pop-up dialog as a reminder? Handling this kind of thing through the visual tree is the Correct(tm) way to do it.
I wouldn't accept a PR from a coworker on my WPF project with this solution. But I can't seem to find any better way in WinUI 3.
Have I missed something about WinUI 3? Is it just not mature enough yet to have a solution? It seems like this scenario isn't so uncommon that it would be completely unsupported.
In this case, I'd create an ICommand dependency property, DeleteCommand and and bind a command in the view model. Here's a sample code using the CommunityToolkit.Mvvm NuGet package.
MainWindow.xaml
The MainWindow is named, "ThisWindow" in this case, so we can access its ViewModel from the ItemTemplate.
The DeleteCommandParameter is bound to the DataContext of the item, ToDoTaskViewModel in this case.
<Window
x:Class="ToDoApp.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:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="ThisWindow"
mc:Ignorable="d">
<Grid RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Horizontal">
<TextBox x:Name="NewToDo" />
<Button
Command="{x:Bind ViewModel.AddToDoCommand}"
CommandParameter="{x:Bind NewToDo.Text, Mode=OneWay}"
Content="Add" />
</StackPanel>
<ScrollViewer Grid.Row="1">
<ItemsRepeater ItemsSource="{x:Bind ViewModel.ToDoTasks, Mode=OneWay}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="local:ToDoTaskViewModel">
<local:ToDoTaskView
DeleteCommand="{Binding ElementName=ThisWindow, Path=ViewModel.DeleteToDoCommand}"
DeleteCommandParameter="{x:Bind}"
ToDo="{x:Bind ToDo, Mode=OneWay}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</Window>
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
namespace ToDoApp;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public MainWindowViewModel ViewModel { get; } = new();
}
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
namespace ToDoApp;
[ObservableObject]
public partial class MainWindowViewModel
{
[ObservableProperty]
private ObservableCollection<ToDoTaskViewModel> toDoTasks = new();
[RelayCommand]
private void AddToDo(string todo)
{
ToDoTasks.Add(new ToDoTaskViewModel() { ToDo = todo });
}
[RelayCommand]
private void DeleteToDo(ToDoTaskViewModel toDoTask)
{
ToDoTasks.Remove(toDoTask);
}
}
ToDoTaskView.xaml
<UserControl
x:Class="ToDoApp.ToDoTaskView"
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:local="using:ToDoApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
Text="{x:Bind ToDo, Mode=OneWay}" />
<Button
Grid.Column="1"
Command="{x:Bind DeleteCommand, Mode=OneWay}"
CommandParameter="{x:Bind DeleteCommandParameter, Mode=OneWay}"
Content="Delete" />
</Grid>
</UserControl>
ToDoTaskView.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Windows.Input;
namespace ToDoApp;
public sealed partial class ToDoTaskView : UserControl
{
public static readonly DependencyProperty ToDoProperty = DependencyProperty.Register(
nameof(ToDo),
typeof(string),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandProperty = DependencyProperty.Register(
nameof(DeleteCommand),
typeof(ICommand),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public static readonly DependencyProperty DeleteCommandParameterProperty = DependencyProperty.Register(
nameof(DeleteCommandParameter),
typeof(object),
typeof(ToDoTaskView),
new PropertyMetadata(default));
public ToDoTaskView()
{
this.InitializeComponent();
}
public string ToDo
{
get => (string)GetValue(ToDoProperty);
set => SetValue(ToDoProperty, value);
}
public ICommand DeleteCommand
{
get => (ICommand)GetValue(DeleteCommandProperty);
set => SetValue(DeleteCommandProperty, value);
}
public object DeleteCommandParameter
{
get => (object)GetValue(DeleteCommandParameterProperty);
set => SetValue(DeleteCommandParameterProperty, value);
}
}
ToDoTaskViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ToDoApp;
[ObservableObject]
public partial class ToDoTaskViewModel
{
[ObservableProperty]
private string toDo = string.Empty;
}
Ok I have a solution. I cannot emphasize enough how much of a disgusting hack this is. Normally I'd be embarrassed to post this, but the only ones who should be embarrassed are Microsoft for publishing Win UI 3 in its current state and claiming it's capable of making real applications.
The gist of this is to mimic Ancestor-type RelativeSource binding in WPF. We create two attached properties - ParentContextViewType to specify the type of the ancestor we're looking for - and ParentContextView which is automatically assigned a reference to the desired parent view instance when the child loads. (I'd have made ParentContextView a readonly property, but of course, Win UI doesn't support that...) Then for the child button, we do a RelativeSource Self binding to the attached ParentContextView property, then adding the rest of the path, just like we would with a legit ancestor type bind.
Here goes (and may god have mercy on my soul):
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace ParentBinding
{
public static class Hacks
{
public static DependencyProperty ParentContextView =
DependencyProperty.RegisterAttached(
"ParentContextView",
typeof(FrameworkElement),
typeof(Hacks),
new PropertyMetadata(null));
public static FrameworkElement GetParentContextView(DependencyObject d)
{
return d.GetValue(ParentContextView) as FrameworkElement;
}
public static void SetParentContextView(DependencyObject d, FrameworkElement view)
{
d.SetValue(ParentContextView, view);
}
public static DependencyProperty ParentContextViewTypeProperty =
DependencyProperty.RegisterAttached(
"ParentContextViewType",
typeof(Type),
typeof(Hacks),
new PropertyMetadata(null, (d, e) =>
{
if (!(d is FrameworkElement fe))
return;
if (e.OldValue != null)
fe.Loaded -= OnParentContextFeLoaded;
if (e.NewValue != null)
fe.Loaded += OnParentContextFeLoaded;
}));
private static void OnParentContextFeLoaded(object sender, RoutedEventArgs e)
{
if (!(sender is FrameworkElement fe))
return;
var type = GetParentContextViewType(fe);
if (type == null)
return;
while (!type.IsAssignableFrom(fe.GetType()) &&
(fe = VisualTreeHelper.GetParent(fe) as FrameworkElement) != null)
{
}
SetParentContextView(sender as DependencyObject, fe);
}
public static Type GetParentContextViewType(DependencyObject d)
{
return d.GetValue(ParentContextViewTypeProperty) as Type;
}
public static void SetParentContextViewType(DependencyObject d, Type val)
{
d.SetValue(ParentContextViewTypeProperty, val);
}
}
}
A use-case:
Model stuff:
using Microsoft.UI.Xaml.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace ParentBinding
{
public class Command : ICommand
{
Action<object> _action;
public Command(Action<object> action)
{
_action = action;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
_action?.Invoke(parameter);
}
}
public class Parent
{
public ObservableCollection<Child> Children { get; set; }
private Command _deleteChildCommand;
public ICommand DeleteChildCommand =>
_deleteChildCommand ?? (_deleteChildCommand = new Command((p) =>
{
if (!(p is Child ch))
return;
this.Children.Remove(ch);
}));
}
public class Child
{
public string Name { get; set; }
public override string ToString() => this.Name;
}
}
Main Window:
<Window x:Class="ParentBinding.MainWindow"
x:Name="_main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ParentBinding"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ListView DataContext="{Binding ElementName=_main, Path=Parent}"
ItemsSource="{Binding Children}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Child">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" />
<Button local:Hacks.ParentContextViewType="ListView"
Grid.Column="1"
CommandParameter="{Binding}"
Content="Delete"
Command="{Binding
Path=(local:Hacks.ParentContextView).DataContext.DeleteChildCommand,
RelativeSource={RelativeSource Self}}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Window>
using Microsoft.UI.Xaml;
namespace ParentBinding
{
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
public Parent Parent { get; } = new Parent
{
Children = new System.Collections.ObjectModel.ObservableCollection<Child>
{
new Child
{
Name = "Larry"
},
new Child
{
Name = "Curly"
},
new Child
{
Name = "Moe"
}
}
};
}
}
Amazingly, it works, and one of the reasons I was so curious to try it and post it is that it is, more or less, a general purpose substitute for ancestor type binding in WinUI 3. Hope someone finds it useful.
I'm working on a WinUI app (c# and xaml) with multiple frames and pages.
The problem is that I need to modify a UIElement property (TextBox.Text) from another class. I've been trying so many things and none of them has worked yet.
I'd be glad if someone could inspire me with some useful ways to do it. It can be anything aside from xaml data binding (<property={"Binding bindingName"}).
Thanks for the help.
In this sample code, I have 2 pages (Page1 and Page2) and 1 ViewModel (MainViewModel). Each page has a TextBox that are bound to the same property in the MainViewModel.
If you change the text in Page1, the text in Page2 will be changed, and vice versa.
NuGet packages
CommunityToolkit.Mvvm
Microsoft.Extensions.DependencyInjection
App.xaml.cs
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
namespace MultiplePagesSingleViewModel;
public partial class App : Application
{
private Window? window;
public App()
{
this.InitializeComponent();
Ioc.Default.ConfigureServices(
new ServiceCollection()
// This needs to be Singleton
.AddSingleton<MainViewModel>()
.BuildServiceProvider());
}
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
this.window = new MainWindow();
this.window.Activate();
}
}
Page1.xaml
<Page
x:Class="MultiplePagesSingleViewModel.Page1"
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:local="using:MultiplePagesSingleViewModel"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<StackPanel>
<TextBlock Text="Page 1" />
<TextBox Text="{x:Bind ViewModel.SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Page>
Page1.xaml.cs
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace MultiplePagesSingleViewModel;
public sealed partial class Page1 : Page
{
public Page1()
{
this.InitializeComponent();
// This MainViewModel is the same instance that Page2 gets.
ViewModel = Ioc.Default.GetRequiredService<MainViewModel>();
}
public MainViewModel ViewModel { get; }
}
Page2.xaml
<Page
x:Class="MultiplePagesSingleViewModel.Page2"
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:local="using:MultiplePagesSingleViewModel"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<StackPanel>
<TextBlock Text="Page 2" />
<TextBox Text="{x:Bind ViewModel.SomeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Page>
Page2.xaml.cs
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace MultiplePagesSingleViewModel;
public sealed partial class Page2 : Page
{
public Page2()
{
InitializeComponent();
// This MainViewModel is the same instance that Page1 gets.
ViewModel = Ioc.Default.GetRequiredService<MainViewModel>();
}
public MainViewModel ViewModel { get; }
}
MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace MultiplePagesSingleViewModel;
// This class needs to be "partial" for CommunityToolkit.Mvvm.
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
// The CommunityToolkit.Mvvm will automatically generate
// a UI-Interactive "SomeText" property for you.
private string someText = "Default text";
}
MainWindow.xaml
<Window
x:Class="MultiplePagesSingleViewModel.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:local="using:MultiplePagesSingleViewModel"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,*">
<local:Page1 Grid.Column="0" />
<local:Page2 Grid.Column="1" />
</Grid>
</Window>
Well, finally after two weeks of research I've found and check a solution.
The solution is basically rising and catching events from the different classes. I found this information in a MSDN post. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-publish-events-that-conform-to-net-framework-guidelines
This post is REALLY STRAIGHT FORWARD. I've created the CustomEventArgs class and then the publisher class is my controller (.cs) and my subscriber is the model (.xaml.cs).
The only problem that I've encountered is setting alll the methods and attributes as static in order to be able to call the controller function from other models. The problem is that on the raiseEvent(this, e); in the publisher class, this cannot be static so i've written null instead.
Also, I'd like to say that it works extremely fluent and with no lag or delay at all even if I constantly use it.
I hope this helps to everybody encountering the same problem.
I have two xaml toggles in separate files that I want to update simultaneously (if one is switched on the other should be too (and vice versa). My first switch in xaml is:
<Switch Grid.Column="1" x:Name="toggleSwitch1" IsToggled="true" Toggled="OnToggled"/>
Using C# how can I return a boolean value of this switch so that I can update another switch simultaneously? Then once retrieving the value, how can I update the xaml of the toggle status for the other switch?
Your Switch control means, as I can understand, that you using UWP, but I'm not sure.
Anyway, the idea is to bind both controls IsToggled properties to same property of some ViewModel:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MyWPFApp
{
public class ControlsViewModel : INotifyPropertyChanged
{
private bool switchToggled;
public bool SwitchToggled
{
get => switchToggled;
set
{
switchToggled = value;
OnPropertyChanged(nameof(SwitchToggled));
}
}
public ControlsViewModel() { }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then in XAML of both Windows set bindings to Switch control (in my example - CheckBox control):
<!-- Window 1 -->
<Window x:Class="MyWPFApp.Window1"
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:MyWPFApp"
mc:Ignorable="d"
Title="Window 1" Height="100" Width="300">
<Grid>
<CheckBox Content="Window1 CheckBox"
IsChecked="{Binding SwitchToggled}"/>
<!-- Replace IsChecked to IsToggled property -->
</Grid>
</Window>
<!-- Window 2 -->
<Window x:Class="MyWPFApp.Window2"
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:MyWPFApp"
mc:Ignorable="d"
Title="Window 2" Height="100" Width="300">
<Grid>
<CheckBox Content="Window2 CheckBox"
IsChecked="{Binding SwitchToggled}"/>
<!-- Replace IsChecked to IsToggled property -->
</Grid>
</Window>
Code-behind of both Windows in example is same:
using System.Windows;
namespace MyWPFApp
{
public partial class Window1 : Window // or public partial class Window2
{
public Window1(ControlsViewModel cvm) // or public Window2
{
InitializeComponent();
DataContext = cvm;
}
}
}
And when calling that example Windows to show from Main one, you creating ControlsViewModel instance and pass it to both:
using System.Windows;
namespace MyWPFApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var cvm = new ControlsViewModel();
new Window1(cvm).Show();
new Window2(cvm).Show();
}
}
}
So checking/unchecking (toggle/untoggle) one of them will affect another and vice versa. Also, you can change SwitchToggled from code somewhere, which would affect both controls too.
Please note, that this is just example to try explain the idea. More MVVM pattern explanations and examples you can find at MSDN.
I'm currently learning how MVVM works and gettings a bit confused.
What I Have Now: I've got a MainWindow.xaml and have made a button that adds in UserControl1.xaml adding it to a ContentControl, which all works great. I've got a folder named ViewModels with a class named SettingsViewModel.cs and another folder named Views with a UserControl named SettingsView.xaml
What I'm trying to figure out: In the User Control I'll have things like buttons, checkboxes, and some other stuff, I want to be able to have a button press in the MainWindow to call a method where I can do stuff like changing the visibility of items among other things. How I go about calling this method from the MainWindow and where to put the method [SettingsViewModels.cs or SettingsView.xaml].
I'm still very new to programming so I'm probability leaving out a bunch of info, so ask me any question.
I have accually got this to work the other way around; calling a method in MainWindow from a UserControl like this...
//this is in the UserControl
private void Button1_Click(object sender, RoutedEventArgs e)
{
MainWindow callMethod = (MainWindow)Application.Current.MainWindow;
callMethod.MyMethod1();
}
//this is in the MainWindow
pubic void MyMethod1()
{
//whatevery i want here
}
There are a couple of things to consider. In MVVM, View communicate to ViewModel through bindings and ViewModel communicate to the View through events typical from INotifyPropertyChanged and ICollectionChanged. Buttons should be binded to a property of type ICommand. The ViewModel should not know about WPF control stuff like Visibility etc.
To change visibility you use an IValueConverter called BooleanToVisiblityConverter.
Without quite understanding what you are asking, here is some pseudo code of how I would do it.
The structure of your files doesn't matter, but dividing them into Views and ViewModels is a good idea.
Disclaimer: This code will not run, shows only the concept. I left Visual Studio on my other computer.
ViewModel:
public class MainWindowViewModel
{
public ICommand OpenCommand { get; }
public object Child { get; private set; }
public MainWindowViewModel()
{
OpenCommand = new RelayCommand(Open);
}
private void DoOpen()
{
Child = new ChildViewModel();
}
}
public class ChildViewModel
{
public bool ShowSomething { get; }
}
public class Program
{
private void SomeStartupLogic()
{
var window = new MainWindow();
windows.DataContext = new MainWindowViewModel(); // or use an IoC container
window.Show();
}
}
View
<Window class="MainWindow">
<Window.Resources>
<DataTemplate DataType="{x:Type ChildViewModel}">
<ChildView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding Child}"/>
<Button Command="{Binding OpenCommand}"/>
</Grid>
</Window>
<UserControl class="ChildView">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConvert"/>
</UserControl.Resources>
<Grid>
<TextBlock Text="Something" Visibility="{Binding ShowSomething, Converter={StaticResource BooleanToVisibilityConvert}/>
</Grid>
</UserControl>
Links
MVVM
Commands
PropertyChanged
In my XAML I am doing the following
<Label Content="{Binding ElementName=Root, Path=UserData.Email, Mode=OneWay}" />
the Root element is my Window itself and the UserData Is a get; private set; auto property in my codebehind file, the Email property is get-only and is of type string.
the UserData object gets set after the user has logged in. But the binding is not taking the value from the object. I have verified that the object does indeed contain the correct data and isn't null. What am I missing here?
I went ahead and created a hello world version for this. Here is the xml. This should simply change the banner when the button is clicked to the text in the text box. I couldn't find a super simple example so I just made one. Obviously there are way more advanced ways to do this but it should make for a simple version to build from.
<Window x:Class="Hello_World.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Label Name="MyLabel" Content="{Binding MyLabel}" HorizontalAlignment="Left" Margin="58,37,0,0" VerticalAlignment="Top" Height="65" Width="423" FontSize="44"/>
<TextBox Name="MyTextBox" HorizontalAlignment="Left" Height="28" Margin="163,162,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="163"/>
<Button Content="Change Banner" HorizontalAlignment="Left" Margin="251,209,0,0" VerticalAlignment="Top" Width="109" Click="Button_Click"/>
</Grid>
</Window>
Next is the ModelView that implements the INotifyPropertyChanged interface. Note that your properties must be public properties with a getter, setter and backing field. This allows you to call the OnPropetyChanged() method whenever the property is set.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Hello_World
{
public class MainViewModel: INotifyPropertyChanged
{
private string _myLabel;
public string MyLabel
{
get { return _myLabel; }
set
{
_myLabel = value;
OnPropertyChanged(nameof(MyLabel));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propetyName)
{
if(PropertyChanged != null)
PropertyChanged(this,new PropertyChangedEventArgs(propetyName));
}
}
}
Lastly the MainWindow. Set the DataContext in the main constructor. Note I could have set the DataContext of the main grid and all of its children would inherit the same DataContext. This would keep you from having to set all of the components' individually.
namespace Hello_World
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainViewModel MyViewModel;
public MainWindow()
{
InitializeComponent();
MyViewModel = new MainViewModel();
// Here's where I'm setting the object to look at.
DataContext = MyViewModel;
// Now I don't need to access the textbox directly.
MyViewModel.MyLabel = "Hello World";
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Note: ICommand is a more advanced topic.
MyViewModel.MyLabel = MyTextBox.Text;
}
}
}