Changing the visibility of an UserControl by PropertyChangedEvent - c#

Update: Full working example:
I am using property binding to change the visibility of an usercontrol in my window. With the INotifyPropertyChanged i notify the UI to update.
I am using this RelayCommand implementation.
C#
//ViewModel:
public class homeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Visibility _Home;
public Visibility Home
{
get { return _Home; }
set {
if(_Home == value) return;
_Home = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Home"));
}
}
public ICommand HideHomeCommand
{
get {
return new RelayCommand(
o => HideUserControl,
o => {return Home != Visibility.Collapsed;}
);
}
}
private void HideUserControl()
{
Home = Visibility.Collapse;
}
public ICommand ShowHomeCommand
{
get {
return new RelayCommand(
o => ShowUserControl,
o => {return Home != Visibility.Visible;}
);
}
}
private void ShowUserControl()
{
Home = Visibility.Visible;
}
}
XAML:
<Window ... >
<Window.DataContext>
<vm:homeViewModel />
</Window.DataContext>
<StackPanel>
<Button Content="Show Home" Command="{Binding ShowHomeCommand}" />
<Button Content="Collapse Home" Command="{Binding HideHomeCommand}" />
<views:home Visibility="{Binding Home }" />
<!-- home is an usercontrol -->
</StackPanel>
</Window>

Since you bind visibility Home with <views:home Visibility="{Binding Home}"/> you should use:
PropertyChanged(this, new PropertyChangedEventArgs("Home"));
This way you invoke property changed event for Home property. Also there is no need for _myHome to be a property. It should be a simple private field.
There is a library called PropertyChanged.Fody which handles all these property changed events automatically, you may want to look at it.
If you want to handle PropertyChanged events manually and you are using C# 6.0 (Visual Studio 2015), you may want to use:
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Home)));.
With previous code if you rename Home property and forget to change the string in PropertyChanged event it will silently fail to notify. But with this code the compilation will fail and you will have to fix it.

Related

C# WPF Navigation Between Pages (Views)

I'm attempting to create a class & method which could be used on any window and page to change the current page displayed on the MainWindow window.
So far I got:
class MainWindowNavigation : MainWindow
{
public MainWindow mainWindow;
public void ChangePage(Page page)
{
mainWindow.Content = page;
}
}
The main window itself:
public MainWindow()
{
InitializeComponent();
MainWindowNavigation mainWindow = new MainWindowNavigation();
mainWindow.ChangePage(new Pages.MainWindowPage());
}
Unfortunately this ends up with System.StackOverflowException.
The main reason for creating this is that I want to be able to change the mainWindow.Content from a page which is currently displayed in mainWindow.Content.
I have already reviewed MVVM but I don't think it is worth using it for a small application like this as all I want it to do is Display a Welcome Page on open, then on the side there will be few buttons. Once pressed the mainWindow.Content correctly changes to a page where a user can enter login detail and then on the button press on the login page I want to change the mainWindow.Content to a different page on successful validation of the login details entered.
Using MVVM is absolutely fine as it will simplify the implementation of your requirement. WPF is build to be used with the MVVM pattern, which means to make heavy use of data binding and data templates.
The task is quite simple. Create a UserControl (or DataTemplate) for each view e.g., WelcomePage and LoginPage with their corresponding view models WelcomePageViewModel and LoginPageViewModel.
A ContentControl will display the pages.
The main trick is that, when using an implicit DataTemplate (a template resource without an x:Key defined), the XAML parser will automatically lookup and apply the correct template, where the DataType matches the current content type of a ContentControl. This makes navigation very simple, as you just have to select the current page from a collection of page models and set this page via data binding to the Content property of the ContentControl or ContentPresenter:
Usage
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type WelcomePageviewModel}">
<WelcomPage />
</DataTemplate>
<DataTemplate DataType="{x:Type LoginPageviewModel}">
<LoginPage />
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- Page navigation -->
<StackPanel Orientation="Horizontal">
<Button Content="Show Login Screen"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.LoginPage}" />
<Button Content="Show Welcome Screen"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.WelcomePage}" />
</StackPanel>
<!--
Host of SelectedPage.
Automatically displays the DataTemplate that matches the current data type
-->
<ContentControl Content="{Binding SelectedPage}" />
<StackPanel>
</Window>
Implementation
Create the individual page controls (the controls that host the page content). This can be a Control, UserControl, Page or simply a plain DataTemplate:
WelcomePage.xaml
<UserControl>
<StackPanel>
<TextBlock Text="{Binding PageTitle}" />
<TextBlock Text="{Binding Message}" />
</StackPanel>
</UserControl>
LoginPage.xaml
<UserControl>
<StackPanel>
<TextBlock Text="{Binding PageTitle}" />
<TextBox Text="{Binding UserName}" />
</StackPanel>
</UserControl>
Create the page models:
IPage.cs
interface IPage : INotifyPropertyChanged
{
string PageTitel { get; set; }
}
WelcomePageViewModel.cs
class WelcomePageViewModel : IPage
{
private string pageTitle;
public string PageTitle
{
get => this.pageTitle;
set
{
this.pageTitle = value;
OnPropertyChanged();
}
}
private string message;
public string Message
{
get => this.message;
set
{
this.message = value;
OnPropertyChanged();
}
}
public WelcomePageViewModel()
{
this.PageTitle = "Welcome";
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
LoginPageViewModel.cs
class LoginPageViewModel : IPage
{
private string pageTitle;
public string PageTitle
{
get => this.pageTitle;
set
{
this.pageTitle = value;
OnPropertyChanged();
}
}
private string userName;
public string UserName
{
get => this.userName;
set
{
this.userName = value;
OnPropertyChanged();
}
}
public LoginPageViewModel()
{
this.PageTitle = "Login";
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Create an enumeration of page identifiers (to eliminate magic strings in XAML and C#):
PageName.cs
public enum PageName
{
Undefined = 0, WelcomePage, LoginPage
}
Create the MainViewModel which will manage the pages and their navigation:
MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class MainViewModel
{
public ICommand SelectPageCommand => new RelayCommand(SelectPage);
private Dictionary<PageName, IPage> Pages { get; }
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
this.selectedPage = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
this.Pages = new Dictionary<PageName, IPage>
{
{ PageName.WelcomePage, new WelcomePageViewModel() },
{ PageName.LoginPage, new LoginPageViewModel() }
};
this.SelectedPage = this.Pages.First().Value;
}
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
this.SelectedPage = selectedPage;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
You probably want to define MainWindowNavigation as a static class with a method that simply changes the Content of the current MainWindow:
static class MainWindowNavigation
{
public static void ChangePage(Page page)
{
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
if (mainWindow != null)
mainWindow.Content = page;
}
}
You can then call the method from any class without having a reference to the MainWindow:
MainWindowNavigation.ChangePage(new Pages.MainWindowPage());

Why isn't my textbox updating the CanExecute value for my WPF button command?

I am trying to learn WPF by implementing a simple button and textbox. I want to understand why my buttons IsEnabled state isn't updating based on the value of my text field.
XAML:
<TextBox Height="100"
TextWrapping="Wrap"
Text="{Binding Test,NotifyOnSourceUpdated=True,NotifyOnTargetUpdated=True}"
VerticalAlignment="Top"
Padding="5, 3, 1, 1"
AcceptsReturn="True" Margin="161,10,10,0"/>
<Button Content="Go"
IsEnabled="{Binding MyButtonCanExecute}"
Command="{Binding MyButtomCommand}"
HorizontalAlignment="Left"
Margin="64,158,0,0"
VerticalAlignment="Top" Width="75"/>
C#:
class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public bool MyButtonCanExecute
{
get
{
return !String.IsNullOrWhiteSpace(Test);
}
}
private ICommand myButtonCommand;
public ICommand MyButtomCommand
{
get
{
if(myButtonCommand == null)
{
myButtonCommand = new RelayCommand(ShowMessage, param => this.MyButtonCanExecute);
}
return myButtonCommand;
}
}
private string test;
public string Test
{
get { return this.test; }
set
{
if (this.test != value)
{
this.test = value;
this.NotifyPropertyChanged("Test");
}
}
}
public MainWindowViewModel()
{
//
}
public void ShowMessage(object obj)
{
MessageBox.Show("Value of textbox is set to: " + this.Test);
}
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Questions:
When I type into the textbox, my breakpoint in the Test setter does not get hit. Why? If the textbox is bound to the Test property, isn't that the point?
When I type into the textbox, the MyButtonCanExecute gets called constantly. However, in debug the value of test is always null... why? Shouldn't it take whatever I type into the textbox?
The main issue seems to be that the value of Test isn't updating whenever I type.
I understand there may be a different way to implementing binding the IsEnabled state to the length of test, but I want to understand what's wrong with my understanding of how WPF works.
Answering my own question for future readers. Thanks for #ASh for pointing me in the right direction.
The problem was that, when typing into the textbox, the UpdateSourceTrigger had its default value (because I hadn't set it in the XAML). This meant that the property the textbox was bound to didn't update until the element lost focus, rather than when its text changed.
The solution:
Change the Text field of the textbox to this:
Text="{Binding Test,UpdateSourceTrigger=PropertyChanged}"
Note the new UpdateSourceTrigger=PropertyChanged means the source property (MainWindowViewModel.Test) updates every time I type.
Then, inside the Test property setting I had to add a new NotifyPropertyChanged call for the MyButtonCanExecute property which is dependent on the value of Test:
private string test;
public string Test
{
get { return this.test; }
set
{
if (this.test != value)
{
this.test = value;
this.NotifyPropertyChanged("Test");
this.NotifyPropertyChanged("MyButtonCanExecute");
}
}
}
And also add the UpdateSourceTrigger to the IsEnabled value of the button:
<Button Content="Go"
IsEnabled="{Binding MyButtonCanExecute,UpdateSourceTrigger=PropertyChanged}"
Command="{Binding MyButtomCommand}" />
EDIT:
A better solution is to remove the IsEnabled binding altogether, so you just have:
<Button Content="Go"
Command="{Binding MyButtomCommand}" />
This is because when we create the MyButtonCommand we pass in MyButtonCanExecute as the CanExecute property to the RelayCommand:
myButtonCommand = new RelayCommand(ShowMessage, param => this.MyButtonCanExecute);

How to add a Property/Attribute to UserControl with no Code behind?

First up, is it possible to add a property to a WPF UserControl with no code behind?
If not, lets say I have a custom UserControl like this:
<UserControl x:Class="Example.Views.View"
xmlns:vm ="clr-Example.ViewModels"
xmlns:view ="clr-Example.Views"
... >
<UserControl.DataContext>
<vm:ViewModel/>
</UserControl.DataContext>
<Button Background="Transparent" Command="{Binding ClickAction}">
<Grid>
...
<Label Content="{Binding Description}"/>
</Grid>
</Button>
</UserControl>
With The ViewModel like this
public class ViewModel : INotifyPropertyChanged
{
private ICommand _clickAction;
public ICommand ClickAction
{
get { return _clickAction; }
set
{
if (_clickAction != value)
{
_clickAction = value;
RaisePropertyChanged("ClickAction");
};
}
}
private int _description;
public int Description
{
get { return _description; }
set
{
if (_description!= value)
{
_description = value;
RaisePropertyChanged("Description");
};
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I want to be able to set the Action like this:
...
<UserControl.Resources>
<ResourceDictionary>
<command:ButtonGotClicked x:Key="gotClicked" />
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<view:FuelDispenserView ClickAction="{StaticResource gotClicked}"/>
</Grid> ...
Without Code behind.
Currently I use this ugly code to achive my goal but I don't like it.
public partial class View : UserControl
{
public View()
{
InitializeComponent();
}
public ICommand ClickAction {
get {
return ((ViewModel)(this.DataContext)).ClickAction;
}
set {
((ViewModel)(this.DataContext)).ClickAction = value;
}
}
}
Does anybody have a better Idea how to do this?
P.S. This is not just meant for this Action. I have different Properties I need to add.
You can use attached properties logic to add custom properties to your user control, but It looks like you have to define different behavior for ClickAction in different views, so I'm not sure it would be useful for you. I suggest you to use routed command and command bindings - it may be helpful in this case.

Making AvalonEdit MVVM compatible

I'm trying to make Avalon MVVM compatible in my WPF application. From googling, I found out that AvalonEdit is not MVVM friendly and I need to export the state of AvalonEdit by making a class derived from TextEditor then adding the necessary dependency properties. I'm afraid that I'm quite lost in Herr Grunwald's answer here:
If you really need to export the state of the editor using MVVM, then I suggest you create a class deriving from TextEditor which adds the necessary dependency properties and synchronizes them with the actual properties in AvalonEdit.
Does anyone have an example or have good suggestions on how to achieve this?
Herr Grunwald is talking about wrapping the TextEditor properties with dependency properties, so that you can bind to them. The basic idea is like this (using the CaretOffset property for example):
Modified TextEditor class
public class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(MvvmTextEditor),
// binding changed callback: set value of underlying property
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.CaretOffset = (int)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
public new int CaretOffset
{
get { return base.CaretOffset; }
set { base.CaretOffset = value; }
}
public int Length { get { return base.Text.Length; } }
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
Now that the CaretOffset has been wrapped in a DependencyProperty, you can bind it to a property, say Offset in your View Model. For illustration, bind a Slider control's value to the same View Model property Offset, and see that when you move the Slider, the Avalon editor's cursor position gets updated:
Test XAML
<Window x:Class="AvalonDemo.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:avalonExt="clr-namespace:WpfTest.AvalonExt"
DataContext="{Binding RelativeSource={RelativeSource Self},Path=ViewModel}">
<StackPanel>
<avalonExt:MvvmTextEditor Text="Hello World" CaretOffset="{Binding Offset}" x:Name="editor" />
<Slider Minimum="0" Maximum="{Binding ElementName=editor,Path=Length,Mode=OneWay}"
Value="{Binding Offset}" />
<TextBlock Text="{Binding Path=Offset,StringFormat='Caret Position is {0}'}" />
<TextBlock Text="{Binding Path=Length,ElementName=editor,StringFormat='Length is {0}'}" />
</StackPanel>
</Window>
Test Code-behind
namespace AvalonDemo
{
public partial class TestWindow : Window
{
public AvalonTestModel ViewModel { get; set; }
public TestWindow()
{
ViewModel = new AvalonTestModel();
InitializeComponent();
}
}
}
Test View Model
public class AvalonTestModel : INotifyPropertyChanged
{
private int _offset;
public int Offset
{
get { return _offset; }
set
{
_offset = value;
RaisePropertyChanged("Offset");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You can use the Document property from the editor and bind it to a property of your ViewModel.
Here is the code for the view :
<Window x:Class="AvalonEditIntegration.UI.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvalonEdit="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
Title="Window1"
WindowStartupLocation="CenterScreen"
Width="500"
Height="500">
<DockPanel>
<Button Content="Show code"
Command="{Binding ShowCode}"
Height="50"
DockPanel.Dock="Bottom" />
<AvalonEdit:TextEditor ShowLineNumbers="True"
Document="{Binding Path=Document}"
FontFamily="Consolas"
FontSize="10pt" />
</DockPanel>
</Window>
And the code for the ViewModel :
namespace AvalonEditIntegration.UI
{
using System.Windows;
using System.Windows.Input;
using ICSharpCode.AvalonEdit.Document;
public class ViewModel
{
public ViewModel()
{
ShowCode = new DelegatingCommand(Show);
Document = new TextDocument();
}
public ICommand ShowCode { get; private set; }
public TextDocument Document { get; set; }
private void Show()
{
MessageBox.Show(Document.Text);
}
}
}
source : blog nawrem.reverse
Not sure if this fits your needs, but I found a way to access all the "important" components of the TextEditor on a ViewModel while having it displayed on a View, still exploring the possibilities though.
What I did was instead of instantiating the TextEditor on the View and then binding the many properties that I will need, I created a Content Control and bound its content to a TextEditor instance that I create in the ViewModel.
View:
<ContentControl Content="{Binding AvalonEditor}" />
ViewModel:
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
// ...
private TextEditor m_AvalonEditor = new TextEditor();
public TextEditor AvalonEditor => m_AvalonEditor;
Test code in the ViewModel (works!)
// tests with the main component
m_AvalonEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("XML");
m_AvalonEditor.ShowLineNumbers = true;
m_AvalonEditor.Load(#"C:\testfile.xml");
// test with Options
m_AvalonEditor.Options.HighlightCurrentLine = true;
// test with Text Area
m_AvalonEditor.TextArea.Opacity = 0.5;
// test with Document
m_AvalonEditor.Document.Text += "bla";
At the moment I am still deciding exactly what I need my application to configure/do with the textEditor but from these tests it seems I can change any property from it while keeping a MVVM approach.

what's wrong with my databinding?

I've copied code from the blank panorama project and made some adjustments, but somewhere something ain't right.
I've got my textblock set up:
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding ElementName=CurrentPlaceNow, Path=Temperature}" />
My model looks like this:
public class CurrentPlaceNowModel : INotifyPropertyChanged
{
#region PropertyChanged()
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private string _temperature;
public string Temperature
{
get
{
return _temperature;
}
set
{
if (value != _temperature)
{
_temperature = value;
NotifyPropertyChanged("Temperature");
}
}
}
}
And defined defined in the MainViewModel():
public CurrentPlaceNowModel CurrentPlaceNow = new CurrentPlaceNowModel();
Finally I've added a modifier to a buttonclick:
App.ViewModel.CurrentPlaceNow.Temperature = "foo";
Now, why isn't anything showing up in the textbox?
Your Binding should navigate through the ViewModel. Binding to an ElementName tries to look at another object in the Visual Tree.
Change your Binding to this:
<TextBlock
Grid.Column="0"
Grid.Row="0"
Text="{Binding CurrentPlaceNow.Temperature}" />
Verify your ViewModel's property is formatted properly:
private CurrentPlaceNowModel _CurrentPlaceNow = new CurrentPlaceNowModel();
public CurrentPlaceNowModel CurrentPlaceNow
{
get { return _CurrentPlaceNow; }
set
{
_CurrentPlaceNow = value;
NotifyPropertyChanged("CurrentPlaceNow");
}
}
As long as your View's DataContext is your MainViewModel, you are good to go.
You are using ElementName wrong. ElementName is when you want to bind to another XAML control, not to (view)model.
To bind to model, set instance of that model to DataContext property and bind only Path.

Categories

Resources