I'm just getting started with the toolkit and I'm trying to generate a simple ObservableProperty to use with WPF. I create a usercontrol:
<UserControl x:Class="WPF_test.StatusControl"
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:local="clr-namespace:WPF_test"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBox x:Name="txtTest" Text="{Binding testData}" Grid.Column="0" Grid.Row="0" Margin="5,5,5,5" />
</Grid>
</UserControl>
and a ViewModel:
using System;
using CommunityToolkit.Mvvm;
using CommunityToolkit.Mvvm.ComponentModel;
namespace WPF_test
{
[ObservableObject]
public partial class StatusControlViewModel
{
[ObservableProperty]
private String? testData;
}
}
I embed the control into the MainWindow and set the datacontext in codebehind:
public partial class MainWindow : Window
{
StatusControlViewModel model;
public MainWindow()
{
InitializeComponent();
model = new StatusControlViewModel();
status.DataContext = model;
model.testData = "test";
}
}
but I see that model.testData is inaccessible due to its protection level. When I comment this line out in order to get the code to run I get a binding error saying that testData cannot be found.
This is the generated code:
namespace WPF_test
{
partial class StatusControlViewModel
{
/// <inheritdoc cref="testData"/>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public string? TestData
{
get => testData;
set
{
if (!global::System.Collections.Generic.EqualityComparer<string?>.Default.Equals(testData, value))
{
OnTestDataChanging(value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.TestData);
testData = value;
OnTestDataChanged(value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.TestData);
}
}
}
/// <summary>Executes the logic for when <see cref="TestData"/> is changing.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
partial void OnTestDataChanging(string? value);
/// <summary>Executes the logic for when <see cref="TestData"/> just changed.</summary>
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
partial void OnTestDataChanged(string? value);
}
}
It seems that the toolkit is capitalising my property name. I can make the databinding work by capitalising the property name in the control XAML:
<TextBox x:Name="txtTest" Text="{Binding TestData}" Grid.Column="0" Grid.Row="0" Margin="5,5,5,5" />
and similarly access the model property:
model.TestData = "test";
Is there a way to use the toolkit so that the property is accessed in the original form, i.e.
<TextBox x:Name="txtTest" Text="{Binding testData}" Grid.Column="0" Grid.Row="0" Margin="5,5,5,5" />
not
<TextBox x:Name="txtTest" Text="{Binding TestData}" Grid.Column="0" Grid.Row="0" Margin="5,5,5,5" />
? I think it's going to be confusing otherwise.
No.
There is no way "to use the toolkit so that the property is accessed in the original form"
Because that's a field and not a property.
The code generator is literally generating some code.
The property name cannot be the same as your backing field. testData is a field. You cannot bind to a field.
[ObservableProperty]
private String? testData;
Generates a TestData property in the partial class.
public string? TestData
{
get => testData;
set
{
if (!global::System.Collections.Generic.EqualityComparer<string?>.Default.Equals(testData, value))
{
OnTestDataChanging(value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.TestData);
testData = value;
OnTestDataChanged(value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.TestData);
}
}
}
You can only bind to public properties so you need what that generates in order to bind. No property means no binding.
Binding:
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/?view=netdesktop-6.0
Property:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties
In the above _seconds is a field.
Hours is a property.
The difference is properties have getters and (optional) setters.
Having said all that.
You are not forced to use that attribute.
You can create your properties manually.
Then you can have whatever case you like for your property.
I suggest you learn to like upper case properties though.
I've worked for a lot of clients. The standard for property names has always been to start with an upper case letter.
PS
[Relaycommand] generates an upper class property with "Command" appended.
Related
I am struggling to get a user control to accept a property from my Data Context object. I don't want to pass just the value; but the instance of the property because I would like to have converters operate on the attributes of the property.
I am very new to the WPF space, I've read many articles and none of them don't address this issue. The reason I'm trying to do this is because I have a calculations class that has many properties that need to be displayed and I don't really want to create a user control for each property or have 2,000 lines of repetitious XAML.
Any insight would be greatly appreciated.
Example Class
public class MyClass
{
[MyAttribute("someValue")]
public string Foo { get; set; }
}
View Model
public class MyViewModel : INotifyPropertyChanged
{
private _myClass;
public MyClass MyClass1
{
get => _myClass;
set
{
if(_myClass != value)
{
_myClass = value;
OnPropertyChanged();
}
}
}
}
Parent XAML
<UserControl DataContext="MyViewModel">
<Grid>
<!-- this is where I'm struggling, I think -->
<uc:MyConsumerControl ObjectProp="{Binding Path=MyClass1.Foo}"/>
</Grid>
</UserControl>
User Control
XAML
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
<Grid>
<TextBox Text="{Binding ObjectProp}"/>
<TextBlock Text="{Binding Path=ObjectProp, Converter={StaticResource MyAttrConverter}}"/>
</Grid>
</UserControl>
C#
public class MyConsumer : UserControl
{
public MyConsumer { InitializeComponent(); }
public object ObjectProp
{
get => (object)GetValue(ObjDepProp);
set => SetValue(ObjDepProp, value);
}
public static readonly DependencyProperty ObjDepProp =
DependencyProperty.Register(nameof(ObjectProp),
typeof(object), typeof(MyConsumer));
}
First of all, there is a naming convention for identifier fields of dependency properties:
public static readonly DependencyProperty ObjectPropProperty =
DependencyProperty.Register(nameof(ObjectProp), typeof(object), typeof(MyConsumer));
public object ObjectProp
{
get => GetValue(ObjectPropProperty);
set => SetValue(ObjectPropProperty, value);
}
Second, a UserControl that exposes bindable properties must never set its own DataContext, so this is wrong:
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
The XAML should look like this:
<UserControl ...>
<Grid>
<TextBox Text="{Binding ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
<TextBlock Text="{Binding Path=ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}, />
Converter={StaticResource MyAttrConverter}}"
</Grid>
</UserControl>
Finally, this is also wrong, because it only assigns a string to the DataContext:
<UserControl DataContext="MyViewModel">
It could probably look like shown below - although that would again explicitly set the DataContext of a UserControl, but perhaps one that could be considered a top-level view element like a Window or Page.
<UserControl ...>
<UserControl.DataContext>
<local:MyViewModel/>
</UserControl.DataContext>
<Grid>
<uc:MyConsumerControl ObjectProp={Binding Path=MyClass1.Foo}
</Grid>
</UserControl>
How to create a general user control using MVVM Light?
All the main views in the application seem to work fine. However, general controls doesn't seem to accept bindings. This is my FileDiplay control. An icon and a TextBlock displaying a filename next to it.
Utilization
In one of the main views, I try to bind a FileName inside an ItemsTemplate of an ItemsControl. Specifying a literal, like FileName="xxx" works fine, but binding doesn't.
<local:FileLink FileName="{Binding FileName}" />
I've been playing around with DependencyProperty and INotifyPropertyChanged a lot. And seemingly there's no way around a DependencyProperty, since it can't be bound otherwise. When using a simple TextBlock instead of this user control, binding is accepted.
I didn't include the locator or the utilizing control in order to avoid too much code. In fact, I think this is a very simple problem that I haven't found the solution for, yet. I do think that having the DataContext set to the ViewModel is correct, since no list binding or real UserControl separation is possible. I've also debugged into the setters and tried the different approaches.
FileLink.xaml
<local:UserControlBase
x:Class="....FileLink"
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:local="clr-namespace:..."
mc:Ignorable="d" DataContext="{Binding FileLink, Source={StaticResource Locator}}">
<Grid>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" Margin="0,0,5,0" />
<TextBlock Text="{Binding FileName}" />
</StackPanel>
</Grid>
</local:UserControlBase>
FileLink.xaml.cs
using System.Windows;
using System.Windows.Media;
namespace ...
{
public partial class FileLink : UserControlBase
{
private FileLinkViewModel ViewModel => DataContext as FileLinkViewModel;
public static DependencyProperty FileNameProperty = DependencyProperty.Register(nameof(FileName), typeof(string), typeof(FileLink));
public ImageSource Icon
{
get
{
return App.GetResource("IconFileTypeCsv.png"); // TODO:...
}
}
public string FileName
{
get
{
return ViewModel.FileName;
}
set
{
ViewModel.FileName = value;
}
}
public FileLink()
{
InitializeComponent();
}
}
}
FileLinkViewModel.cs
using GalaSoft.MvvmLight;
namespace ...
{
public class FileLinkViewModel : ViewModelBase
{
private string _FileName;
public string FileName
{
get
{
return _FileName;
}
set
{
Set(() => FileName, ref _FileName, value);
}
}
}
}
Do not explicitly set the DataContext of your UserControl, because it effectively prevents that the control inherits the DataContext from its parent control, which is what you expect in a Binding like
<local:FileLink FileName="{Binding FileName}" />
Also, do not wrap the view model properties like you did with the FileName property. If the view model has a FileName property, the above binding works out of the box, without any wrapping of the view model.
If you really need a FileName property in the UserControl, it should be a regular dependency property
public partial class FileLink : UserControlBase
{
public FileLink()
{
InitializeComponent();
}
public static readonly DependencyProperty FileNameProperty =
DependencyProperty.Register(nameof(FileName), typeof(string), typeof(FileLink));
public string FileName
{
get { return (string)GetValue(FileNameProperty); }
set { SetValue(FileNameProperty, value); }
}
}
and you should bind to it by specifying the UserControl as RelativeSource:
<local:UserControlBase ...> <!-- no DataContext assignment -->
<StackPanel Orientation="Horizontal">
<Image Source="IconFileTypeCsv.png" Margin="0,0,5,0" />
<TextBlock Text="{Binding FileName,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
</local:UserControlBase>
The ViewModel:
public class ConnectionStatusViewModel : BindableBase
{
private string _txtConn;
public string TextConn
{
get { return _txtConn; }
set { SetProperty(ref _txtConn, value); }
}
}
The XAML:
<UserControl x:Class="k7Bot.Login.Views.ConnectionStatus"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://www.codeplex.com/prism"
prism:ViewModelLocator.AutoWireViewModel="True" Width="300">
<Grid x:Name="LayoutRoot">
<Label Grid.Row="1" Margin="10,0,10,0">connected:</Label>
<TextBlock Text="{Binding TextConn}" Grid.Row="1" Grid.Column="1" Margin="10,0,10,0" Height="22" />
</Grid>
</UserControl>
The View:
public partial class ConnectionStatus : UserControl
{
public ConnectionStatus()
{
InitializeComponent();
}
}
In another module, I have an event listener, that eventually runs this code:
ConnectionStatusViewModel viewModel = _connectionView.DataContext as ConnectionStatusViewModel;
if (viewModel != null)
{
viewModel.TextConn = "Testing 123";
}
The code runs but the TextConn is updated and does not display in the UI
Are you sure TextConn does not update? Because it can update but the display could not change. You should implement the INotifyPropertyChanged interface and after you make any changes to TextConn call the implemented OnPropertyChanged("TextConn"); or whatever you name the function. This will tell the UI that the value has changed and it needs to update.
The UserControl's DataContext gets its value when the UC is initialized. Then you get a copy of the DataContext, cast it to a view model object, and change the property. I don't believe that the UC gets its original DataContext updated in this scenario.
Probably you need to use a message mediator to communicated changes between different modules.
After some troubleshooting, this code works, the issue was that I was running this code:
ConnectionStatusViewModel viewModel = _connectionView.DataContext as ConnectionStatusViewModel;
if (viewModel != null)
{
viewModel.TextConn = "Testing 123";
}
before the view was actually activated. Silly, but maybe it will help someone down the line.
I've been working on a sample project using MVVM Light and I'm wondering how to bind a TextBox Text value and have it passed to and from the View to the View Model. This is the first time I've worked with MVVM Light so I'm new to this.
Basically a user will enter a Project name in to the Text Box name and click the New Project button which should generate a database named after what was typed in to the Project Name Text Box.
View :
<UserControl x:Class="Sample.Views.NavigationTree.NewProjectView"
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:mui="http://firstfloorsoftware.com/ModernUI"
xmlns:ignore="http://www.ignore.com"
mc:Ignorable="d ignore"
DataContext="{Binding NewProjectView, Source={StaticResource Locator}}">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<mui:BBCodeBlock BBCode="Project Name"/>
<Label Width="10"/>
<TextBox Text="{Binding ProjName, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="120"/>
</StackPanel>
<Label Height="10"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Label Width="85"/>
<Button Content="New Project" Margin="0,0,3,0" Command="{Binding AddProjectCommand}" IsEnabled="{Binding IsUserAdmin}" Grid.Column="2" Grid.Row="0"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
ViewModel:
using Sample.Model.Database;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Text;
namespace Sample.ViewModel
{
/// <summary>
/// This class contains properties that a View can data bind to.
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class NewProjectViewModel : ViewModelBase
{
private string _projName;
//Binding AddProjectCommand
public RelayCommand AddProjectCommand { get; set; }
private string consoleText { get; set; }
private StringBuilder consoleBuilder = new StringBuilder(360);
/// <summary>
/// Initializes a new instance of the NewProjectViewModel class.
/// </summary>
public NewProjectViewModel()
{
this.AddProjectCommand = new RelayCommand(() => AddProject());
}
public void AddProject()
{
ProjectDbInteraction.CreateProjectDb(_projName);
}
public string ProjName
{
get { return _projName; }
set
{
if (value != _projName)
{
_projName = value;
RaisePropertyChanged("ProjName");
}
}
}
public string ConsoleText
{
get { return consoleText; }
set
{
consoleBuilder.Append(value);
consoleText = consoleBuilder.ToString();
RaisePropertyChanged("ConsoleText");
}
}
}
}
So how do I pass the ProjName binding to and from the View to the View MOdel?
Looks good, you just need to create an association between the View and ViewModel. Basically, set your DataContext of your view to the ViewModel.
You can do this a few ways, i will show two:
1) In the code-behind of your view, you can create an instance of your viewmodel (ViewModel vm=new ViewModel()) as then assign it with this.DataContext=vm;
2) You can XAML Data Templates. Something like this, where Home is a view and HomeVM is viewmodel.
in
<Window
.
.
.
xmlns:HomeView="clr-namespace:Bill.Views"
xmlns:HomeVM="clr-namespace:Bill.ViewModels"
>
<Window.Resources>
<!--Home User Control and View Model-->
<DataTemplate DataType="{x:Type HomeVM:HomeVM}">
<HomeView:Home/>
</DataTemplate>
</Window.Resources>
The first seems more flexible for my normal needs...
Your textbox binding looks correct. What is not shown is how you associate your ViewModel to the datacontext of the page which can ultimately be consumed by the textbox. I would recommend that you do this in the code behind of the page.
public MyViewModel ModelView;
public MainWindow()
{
InitializeComponent();
DataContext = ModelView = new MyViewModel ();
}
Once the page's datacontext is set as shown above, the controls datacontext, if not set, walks up the visual tree of its parent(s) until a datacontext has been set; which is done here on the page, the ultimate parent.
I provide a more robust example on my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding. which can show you how to roll your own MVVM (which is all MVVM light does).
Remove "Mode=OneWay" from your binding of the textbox where the user types the ProjName, this will allow your property to receive the value. Or choose one of the other Modes that do what you want.
OneWay: use this when you want the data in view model to modify the value in your GUI
TwoWay: use this if you want to allow view model to modify the GUI value, or if you want the GUI value changed by the user to be reflected in view model
OneTime: your view model can set the value that is shown in your GUI once, and it will never change again. Only do this if you know you're not going to need to change the value in your view model.
OneWayToSource: This is the opposite of one way -- GUI value affects view model value.
I've got some problem I need some help with. I want to bind the visibility properties from a view model to the xaml elements so I get some visually changes (collapse or show in this case) by just changing the value in the viewmodel.
I got this xaml
<Window x:Class="PampelMuse.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:welcome="clr-namespace:PampelMuse.Views.Welcome"
xmlns:backend="clr-namespace:PampelMuse.Views.Backend"
xmlns:pampelMuse="clr-namespace:PampelMuse" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
Title="PampelMuse" Height="670" Width="864">
<Grid>
<Image HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Source="Resources/Images/Backgrounds/4.jpg" Stretch="UniformToFill" />
<welcome:WelcomeScreen x:Name="UIWelcome" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding ElementName=UiWelcomeVisibility}" />
<backend:BackendUI x:Name="UIBackend" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Visibility="{Binding ElementName=UiBackendVisibility}" />
</Grid>
The visibilities as you can see are binded to the properties UiWelcomeVisibility and UiBackendVisibility in the UIModel. These properties are now defined as followed:
public partial class MainWindow : Window
{
private ViewModel.ViewModel ViewModel = PampelMuse.ViewModel.ViewModel.GetInstance();
public MainWindow()
{
InitializeComponent();
DataContext = ViewModel; // Setting the data context what effects all the xaml elements in this component too, including UIWelcome and BackendUI
ViewModel.UIModel.UiBackendVisibility = Visibility.Collapsed;
}
The ViewModel:
public class ViewModel
{
private static ViewModel instance = new ViewModel();
public UIModel UIModel = UIModel.GetInstance();
public static ViewModel GetInstance()
{
return instance;
}
}
And the UIModel:
public class UIModel
{
private static UIModel instance = new UIModel();
public Visibility UiWelcomeVisibility { get; set; }
public Visibility UiBackendVisibility { get; set; }
public static UIModel GetInstance()
{
return instance;
}
}
I just don't see any coding mistakes here (and I don't get some at runtime in fact) but the BackendUI-visibility-property is not changed by the UiBackendVisibility of UIModel.
Any ideas? Thanks so far.
You are doing the binding wrong. Visibility="{Binding ElementName=UiWelcomeVisibility}" sets the visibility of an element equal to another visual element named "UiWelcomeVisibility". There are two problems with this:
There is no element named "UiWelcomeVisibility" in the first place.
Even if there were, a visual element itself is not a valid value for the Visibility property.
What you want is to databind to the viewmodel instead. Assuming that you have already set the DataContext to the viewmodel, just use
<welcome:WelcomeScreen ... Visibility="{Binding UiWelcomeVisibility}" />