WPF: VM-first, close Window in a MVVM-conform way - c#

in my application I'm opening a Window for an Input form. In my App.xaml I have defined the following:
<DataTemplate DataType="{x:Type ViewModels:EditTicketViewModel}">
<Frame>
<Frame.Content>
<Views:EditTicketView></Views:EditTicketView>
</Frame.Content>
</Frame>
</DataTemplate>
My application also has a Window service for opening windows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DevPortal.Interfaces
{
public interface IWindowService
{
public void ShowWindow(object viewModel, bool showDialog);
}
}
the implementation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DevPortal.Interfaces;
using Syncfusion.Windows.Shared;
using Syncfusion.SfSkinManager;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DevPortal.Services
{
public class WindowService : IWindowService
{
public void ShowWindow(object viewModel, bool showDialog)
{
var window = new ChromelessWindow();
window.ResizeMode = ResizeMode.NoResize;
SfSkinManager.SetTheme(window, new Theme("FluentDark"));
window.Content = viewModel;
window.SizeToContent = SizeToContent.WidthAndHeight;
window.Title = viewModel.GetType().GetProperty("Title").GetValue(viewModel).ToString();
window.ShowIcon = false;
if (showDialog)
{
window.ShowDialog();
} else
{
window.Show();
}
}
}
}
How I open the window (from a viewmodel in the MainView)
[RelayCommand]
private void CreateTicket()
{
App.Current.ServiceProvider.GetService<IWindowService>().ShowWindow(new EditTicketViewModel(), true);
}
What would be the best way to close this window from the ViewModel? Previously i was used to directly create the view, and in the constructor of the view i would subscribe to a close-event in the viewmodel, but that's not really the MVVM-way I guess. Do I need to implement some kind of service? Thanks!
EDIT: I forgott to mention that the View is a page. So i Am creating a window with the viewmodel as content, and the datatemplate of the viewmodel is a Frame containing the page.

You could for example return an IWindow from your window service:
public class WindowService : IWindowService
{
public IWindow ShowWindow(object viewModel, bool showDialog)
{
var window = new ChromelessWindow();
...
return window;
}
}
...and then simply call Close() on this one in the view model.
The interface would be as simple as this:
public interface IWindow
{
void Close();
}
Your ChromelessWindow implements the interface:
public partial class ChromelessWindow : Window, IWindow { ... }
...and the view model only has a dependency on an interface. It still doesn't know anything about a view or actual window. IWindow is just a name. I can be called anything.

The cleanest way of closing a Window from it's ViewModel is using an attached property.
public static class perWindowHelper
{
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached(
"CloseWindow",
typeof(bool?),
typeof(perWindowHelper),
new PropertyMetadata(null, OnCloseWindowChanged));
private static void OnCloseWindowChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
if (!(target is Window view))
{
return;
}
if (view.IsModal())
{
view.DialogResult = args.NewValue as bool?;
}
else
{
view.Close();
}
}
public static void SetCloseWindow(Window target, bool? value)
{
target.SetValue(CloseWindowProperty, value);
}
public static bool IsModal(this Window window)
{
var fieldInfo = typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic);
return fieldInfo != null && (bool)fieldInfo.GetValue(window);
}
}
In the ViewModel create a ViewClosed property
private bool? _viewClosed;
public bool? ViewClosed
{
get => _viewClosed;
set => Set(nameof(ViewClosed), ref _viewClosed, value);
}
then in the View, bind to it using the attached property
<Window
...
vhelp:perWindowHelper.CloseWindow="{Binding ViewClosed}" >
Mode details on my take on MVVM navigation on my blog post.

Related

Textbox readonly property not working in TextBox made by me

I have a textbox (TextBoxMoeda) made by me, but, even when the textbox property readonly is set true. nothing happens. Anyone knows how to implement the code to resolve this problem?. Please help me?
public class TextBoxMoeda : TextBox
{
private double dp;
private string fmt = string.Empty;
private int _CasasDecimais = 0;
[Category("TextBoxMoeda")]
public virtual bool SomenteLeitura
{
get => base.ReadOnly;
set
{
base.ReadOnly = value;
Invalidate();
}
}
//Code Continues .......
}
WPF
You want the IsReadOnly property, not the ReadOnly property.
For example:
Create a new WPF App (.NET Framework) project called DeleteMe
In MainWindow.xaml, delete the Grid
In MainWindow.xaml.cs, use this code:
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace DeleteMe
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var tm = new TextBoxMoeda();
this.AddChild(tm);
tm.SomenteLeitura = true;
}
public class TextBoxMoeda : TextBox
{
[Category("TextBoxMoeda")]
public virtual bool SomenteLeitura
{
get => base.IsReadOnly;
set
{
base.IsReadOnly = value;
}
}
}
}
}
The textbox will be readonly (i.e. I can't type anything into the textbox).
WinForms
The code already appears to work in WinForms. These steps will produce a working project:
Create a new Windows Forms App project called DeleteMeAgain
Overwrite all of the Form1.cs code with this:
using System.ComponentModel;
using System.Windows.Forms;
namespace DeleteMeAgain
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var tm = new TextBoxMoeda();
this.Controls.Add(tm);
tm.SomenteLeitura = true;
}
public class TextBoxMoeda : TextBox
{
[Category("TextBoxMoeda")]
public virtual bool SomenteLeitura
{
get => base.ReadOnly;
set
{
base.ReadOnly = value;
Invalidate();
}
}
}
}
}
The textbox will be readonly (i.e. I can't type anything into the textbox).
Windows Form: User Control
Do this:
Create a new Windows Forms App called DeleteMe3
Set Target Framework to .NET Core 3.1 (Long-term support)
In your project, create a new UserControl with the filename TextBoxStack.cs
Overwrite all of the code for that user control with this:
using System.ComponentModel;
using System.Windows.Forms;
namespace DeleteMe3
{
public partial class TextBoxStack : TextBox
{
[Category("TextBoxStack")]
public virtual bool SomenteLeitura
{
get => base.ReadOnly;
set
{
base.ReadOnly = value;
Invalidate();
}
}
}
}
An error will appear. Go to the source of the error and delete that line.
Build the solution
Add your new user control to Form1
Select the new user control on the form
Change the SomenteLeitura property to True
Run the project. The textbox should be grey and readonly.

Close Windows on TextBox text change using Mvvm, Wpf,C#,Visual Studio

I want to execute a method on TextChange event and for specific text, I want to do something and close the window using MVVM
for example on this part of my code I want to close my window:
if (text.Equals("12345"))
{
//Exit from window
}
I know how can I do it from a button using a command, As I have in the code of the next example.
But how can I pass the window to a running property that binds to a text of the textbox?
or there is another way to close form?
I want to do it on the ViewModel and not the code behind to write my code as MVVM pattern close that I can
my XAML code :
<Window x:Class="PulserTesterMultipleHeads.UserControls.TestWindows"
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:PulserTesterMultipleHeads.UserControls"
mc:Ignorable="d"
Name="MainTestWindow"
Title="TestWindows" Height="450" Width="800">
<Grid>
<StackPanel>
<TextBox Text="{Binding Text,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></TextBox>
<Button Command="{Binding EndTestExit}"
CommandParameter="{Binding ElementName=MainTestWindow}">Exit</Button>
</StackPanel>
</Grid>
</Window>
the code behind XAML :
using PulserTesterMultipleHeads.Classes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace PulserTesterMultipleHeads.UserControls
{
/// <summary>
/// Interaction logic for TestWindows.xaml
/// </summary>
public partial class TestWindows : Window
{
public TestWindows()
{
InitializeComponent();
DataContext = new TestWindowsMV();
}
}
}
View Model code :
using PulserTester.ViewModel.Base;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace PulserTesterMultipleHeads.Classes
{
public class TestWindowsMV : INotifyPropertyChanged
{
private string text;
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
//Exit from window
}
}
}
/// <summary>
/// for the example
/// </summary>
private ICommand _EndTestExit;
public ICommand EndTestExit
{
get
{
if (_EndTestExit == null)
{
_EndTestExit = new GenericRelayCommand<Window>((window) => EndTestExitAction(window));
}
return _EndTestExit;
}
}
private void EndTestExitAction(Window window)
{
window.Close();
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
************************* Edit as the answer ****************************
The change in ModelView :
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
CloseAction();
}
}
}
The change in code behind:
public TestWindows()
{
InitializeComponent();
DataContext = new TestWindowsMV();
if (((TestWindowsMV)DataContext).CloseAction == null)
((TestWindowsMV)DataContext).CloseAction = new Action(this.Close);
}
It is kind of hard with MVVM but what you can do is create an Event inside of your ViewModel and attach a function that will close your window in code behind of your View. Then you will be able to call your event inside of ViewModel whenever you need to close the window (or do something else that is more convenient to do in View rather than in ViewModel)
The cleanest way to close a View from the ViewModel is to use an attached property
public static class perWindowHelper
{
public static readonly DependencyProperty CloseWindowProperty = DependencyProperty.RegisterAttached(
"CloseWindow",
typeof(bool?),
typeof(perWindowHelper),
new PropertyMetadata(null, OnCloseWindowChanged));
private static void OnCloseWindowChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
if (!(target is Window view))
return;
if (view.IsModal())
view.DialogResult = args.NewValue as bool?;
else
view.Close();
}
public static void SetCloseWindow(Window target, bool? value)
{
target.SetValue(CloseWindowProperty, value);
}
public static bool IsModal(this Window window)
{
var fieldInfo = typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic);
return fieldInfo != null && (bool)fieldInfo.GetValue(window);
}
}
which you can then bind to an appropriate property in your ViewModel
<Window
x:Class="...
vhelp:perWindowHelper.CloseWindow="{Binding ViewClosed}">
private bool? _viewClosed;
public bool? ViewClosed
{
get { return _viewClosed; }
set { Set(nameof(ViewClosed), ref _viewClosed, value); }
}
More details on my recent blog post.
You have almost everything you need already implemented. All you really need to do is call private void EndTestExitAction(Window window) from your setter and supply window its value during construction:
public string Text {
get {
return text;
} set {
text = value;
if (text.Equals("12345"))
{
EndTestExitAction(window)
}
}
}
where window is a property of your View Model.

Set DataContext in WPF XAML to a specific object

I started a WPF window from a console application. But I have difficulties with the databinding.
TL;DR: Is it possible to refer to a specific (view)model object in the WPF XAML?
This is my Code:
Console.cs
A console application starts the view with a static function in the void Main() function
static void StartGUI(ViewModelClass viewmodel)
{
Thread thread = new Thread(() => { new Application().Run(new View.MainWnd(viewmodel)); });
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
it gets the viewmodel object which has been initiated in the Main().
ViewModel.cs
The viewmodel is the usual impelemtation of the INotifyPropertyChanged interface with a property to be bound to the view.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ConsoleStartsGUI.Model;
namespace ConsoleStartsGUI.ViewModel
{
public class ViewModelClass : INotifyPropertyChanged
{
ModelClass model = new ModelClass();
public string VMProperty
{
get { return model.TestProperty; }
set
{
model.TestProperty = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
View.xaml
In the view I get my problems: The view should know the same viewmodel as the console application.
Currently I use
<Window.DataContext>
<ViewModel:ViewModelClass/>
</Window.DataContext>
to bind the viewmodelclass to the view (this is the result I clicked in the VS designer) as it usually works when I use a WPF project from begin. But this instantiates a new object, which is not the object of the console application.
View.xaml.cs
using System.Windows;
using ConsoleStartsGUI.ViewModel;
namespace ConsoleStartsGUI.View
{
public partial class MainWnd : Window
{
public MainWnd(ViewModelClass viewmodel)
{
InitializeComponent();
}
}
}
Is there a way, to refer to a specific (view)model object in the XAML?
Best regards,
Martin
Yes, this is possible: Just assign the DataContext from your constructor
class MainWindow : Window
{
public MainWindow(ViewModelClass viewmodel)
{
InitializeComponent();
this.DataContext = viewmodel; // should work before as well as after InitalizeComponent();
}
}
Binding it from the XAML obviously does not work, for some reason.

When using Prism to DeepLink to a row inside a ListView, how can I update the ListView scroll and highlight?

I am using Xamarin Forms with Prism, based on this GitHub sample..
Desired Behavior
Deep link is clicked, showing the detail view:
User presses back button. Scroll and highlight the linked selection (not happening).
None of the OnNavigation events are firing. Is this a bug? How do I accomplish this?
App.Xaml
public partial class App : PrismApplication
{
public App(IPlatformInitializer initializer = null) : base(initializer) { }
protected override async void OnInitialized()
{
InitializeComponent();
await NavigationService.NavigateAsync("MainTabbedPage/NavigationPage/ShowsListPage/DetailPage?show=279121");
//await NavigationService.NavigateAsync("MainTabbedPage/NavigationPage/ShowsListPage");
}
protected override void RegisterTypes()
{
Container.RegisterTypeForNavigation<UpcomingShowsPage>();
Container.RegisterTypeForNavigation<ShowsListPage>(); // <-- Problematic ListView
Container.RegisterTypeForNavigation<DetailPage>();
Container.RegisterTypeForNavigation<MainTabbedPage>();
Container.RegisterTypeForNavigation<NavigationPage>();
Container.RegisterType<ITsApiService, TsApiService>();
}
ShowsListPage.xaml
ContentPage is using the Prism directive: prism:ViewModelLocator.AutowireViewModel="True". (nothing special)
ShowsListPageViewModel.cs
using System.Collections.ObjectModel;
using InfoSeries.Core.Models;
using InfoSeries.Core.Services;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Xamarin.Forms;
namespace DeepNavigation.ViewModels
{
public class ShowsListPageViewModel : BindableBase, INavigationAware
{
private readonly ITsApiService _tsApiService;
private readonly INavigationService _navigationService;
private ObservableCollection<SerieFollowersVM> _highlightSeries;
public ObservableCollection<SerieFollowersVM> HighlightSeries
{
get { return _highlightSeries; }
set { SetProperty(ref _highlightSeries, value); }
}
public ShowsListPageViewModel(ITsApiService tsApiService, INavigationService navigationService)
{
_tsApiService = tsApiService;
_navigationService = navigationService;
}
public void OnNavigatedFrom(NavigationParameters parameters)
{
}
public async void OnNavigatedTo(NavigationParameters parameters)
{
var series = await _tsApiService.GetStatsTopSeries();
HighlightSeries = new ObservableCollection<SerieFollowersVM>(series);
}
public void OnNavigatingTo(NavigationParameters parameters)
{
}
private DelegateCommand<ItemTappedEventArgs> _goToDetailPage;
public DelegateCommand<ItemTappedEventArgs> GoToDetailPage
{
get
{
if (_goToDetailPage == null)
{
_goToDetailPage = new DelegateCommand<ItemTappedEventArgs>(async selected =>
{
NavigationParameters param = new NavigationParameters();
var serie = selected.Item as SerieFollowersVM;
param.Add("show", serie.Id);
await _navigationService.NavigateAsync("DetailPage", param);
});
}
return _goToDetailPage;
}
}
}
}
Question
How can I get the back button to select the list view?
Is there any platform guidance saying that the back button after a deep link must go to the source calling application.. rendering this question useless? (e.g. pop the navigation back to Chrome/Safari)

databinding to changing element property

I'm working on a small WPF application in which I have a combobox that is bound to an ObservableCollection in the code behind:
public Molecule CurrentMolecule { get; set; }
public ObservableCollection<string> Formulas { get; set; }
public MainWindow()
{
CurrentMolecule = new Molecule();
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
DataContext = this;
InitializeComponent();
}
<ComboBox x:Name="cmbFormula" ItemsSource="{Binding Path=Formulas}" SelectionChanged="cmbFormula_SelectionChanged"/>
This works fine to populate my combo box with the CurrentMolecule.FormulasList however if at some point I set CurrentMolecule to a new instance of Molecule the databinding no longer works. Do I need to implement some kind of OnPropertyChanged event so that no matter what the contents of the combo box will stay current with the CurrentMolecule.FormulasList?
You have to implement INotifyPropertyChanged, only then the changes will be updated in UI.
Here are the modifications that I've done to your code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private Molecule _CurrentMolecule;
public Molecule CurrentMolecule
{
get
{
return _CurrentMolecule;
}
set
{
_CurrentMolecule = value;
OnPropertyChanged("CurrentMolecule");
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
}
}
private ObservableCollection<string> _Formulas;
public ObservableCollection<string> Formulas
{
get { return _Formulas; }
set
{
_Formulas = value;
OnPropertyChanged("Formulas");
}
}
public MainWindow()
{
InitializeComponent();
CurrentMolecule = new Molecule();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Edit:
A better approach is to create a ViewModel and then bind it to the DataContext of the Window.
Define a new class called ViewModel as below. Note you might want to change the namespace
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
#region Properties
private Molecule _CurrentMolecule;
public Molecule CurrentMolecule
{
get
{
return _CurrentMolecule;
}
set
{
_CurrentMolecule = value;
OnPropertyChanged("CurrentMolecule");
Formulas = new ObservableCollection<string>(CurrentMolecule.FormulasList.ToList());
}
}
private ObservableCollection<string> _Formulas;
public ObservableCollection<string> Formulas
{
get { return _Formulas; }
set
{
_Formulas = value;
OnPropertyChanged("Formulas");
}
}
#endregion
#region Constructor
public ViewModel()
{
CurrentMolecule = new Molecule();
}
#endregion
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Modify the MainWindow code behind file as below
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
You are probably missing WPF controls datacontext fundamentals. If you are using binding like {Binding CurrentMolecule.FormulasList} or parent controls datacontext is bound to "CurrentMolecule" whenever you swap DataContext of that item, the binding will reset. If you would like to keep the datacontext bound to the FormulasList even when the parent datacontext changes. You need to bind that context directly to FormulasList property and make sure that other parent controls are not using CurrentMolecule as a datacontext, even when its instance changes in your ViewModel.

Categories