Equivalent to WPF DependencyProperties in Prism ViewModels - c#

I understand that the standard way in WPF to expose a custom property in XAML is to define it as DependencyProperty in the View’s code-behind.
However, this only works for DependencyObjects, such as a UserControl. Yet, in clean Prism fashion, my code-behind (i.e., the class deriving from UserControl) is empty, and I deal with all the logic in my view model, which derives from BindableBase, which is not a child class of DependencyObject.
Consider the following XAML fragment:
<MyNamespace:MyCustomView MyProperty={Binding} />
The core of MyCustomViewModel is
private string myProperty;
public string MyProperty {
get { return myProperty; }
set { SetProperty(ref myProperty, value); }
I’m still relatively new to Prism. What do I do to expose a MyProperty, which is defined in my MyCustomViewModel so that I can set it in XAML with a tag similar to that above?
Update
Following #mm8’s answer and our discussion in the corresponding comments, I developed a minimal (non-)working example of what I have in mind. A summary first:
Data model is a list of objects.
Shell must display each of these objects by means of a custom user control for this object type.
A) The shell
A.1) XAML
The XAML is straightforward.
<Window x:Class="MyProject.Views.MainWindow"
Name="MainWindowName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
xmlns:MyNamespace="clr-namespace:MyProject.Views"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding StringCollection, ElementName=MainWindowName}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<MyNamespace:MyUserControl MyTargetProperty="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
A.2) Code-behind
The code-behind contains a data model definition; in reality, I’d define this in the Models namespace, of course.
using System.Collections;
using System.Windows;
namespace MyProject.Views {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
StringCollection = new ArrayList();
StringCollection.Add("String 1");
StringCollection.Add("String 2");
StringCollection.Add("String 3");
}
private ArrayList stringCollection;
public ArrayList StringCollection {
get { return stringCollection; }
set { stringCollection = value; }
}
}
}
A.3) View model
The view model is the standard one provided with the Prism code templates.
using Prism.Mvvm;
namespace MyProject.ViewModels {
public class MainWindowViewModel : BindableBase {
private string _title = "Prism Unity Application";
public string Title {
get { return _title; }
set { SetProperty(ref _title, value); }
}
public MainWindowViewModel() {
}
}
}
B) The custom user control
This is where the fun starts. In the end, I’d like to have access to the MyTargetProperty in the MyUserControlViewModel, since I want to invoke sophisticated program logic on it that depends on other work with the data model, and is thus not to be placed in the code-behind.
B.1) XAML
Very naive; only contains a label.
<UserControl x:Class="MyProject.Views.MyUserControl"
Name="UserControlName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Label Content="{Binding MyTargetProperty, ElementName=UserControlName}" Background="AliceBlue"/>
</UserControl>
B.2) Code-behind
This is where I declare the target property as DependencyProperty, as suggested in #mm8’s answer.
using System.Windows;
using System.Windows.Controls;
namespace MyProject.Views {
/// <summary>
/// Interaction logic for MyUserControl
/// </summary>
public partial class MyUserControl : UserControl {
public MyUserControl() {
InitializeComponent();
}
public static readonly DependencyProperty MyTargetPropertyProperty = DependencyProperty.Register("MyTargetProperty", typeof(string), typeof(MyUserControl));
public string MyTargetProperty {
get { return (string)GetValue(MyTargetPropertyProperty); }
set { SetValue(MyTargetPropertyProperty, value); }
}
}
}
B.3) View model
The view model defines the source property.
using Prism.Mvvm;
namespace MyProject.ViewModels {
public class MyUserControlViewModel : BindableBase {
public MyUserControlViewModel() {
}
private string mySourceProperty;
public string MySourceProperty {
get { return mySourceProperty; }
set { SetProperty(ref mySourceProperty, value); }
}
}
}
I can’t for the life of me figure out how to access the values I set in the MainWindow’s ItemTemplate within the MyUserControl’s view model.

Only target (view) properties must be dependency properties. So for you to be able to bind anything to such a property, it must be a dependency property like MyProperty in this case:
<MyNamespace:MyCustomView MyProperty="{Binding SourceProperty}" />
A source property in a view model may however be a plain CLR property:
public string SourceProperty { get; set; }
So your view models don't have to (and shouldn't!) inherit from DependencyObject but views should.

Related

Saving and accessing txt file in WPF application

I am new to creating apps. I am trying to build a WPF app that requires me to have a combobox with hundreds of items. I have all of these items saved in a txt/excel file. Obviously, I don't want to hardcode all the options in XAML/C#.
I have no idea how to go about this. I tried to store the list as a resource but don't know how to access the resource once I've put it in there.
I am looking for the easiest approach that can accomplish what I need.
Thanks!
To get the values in your txt/csvfile is trivial (What's the fastest way to read a text file line-by-line?). To get these values to update your ComboBox's contents is actually fairly involved, but the process is at the heart of WPF and MVVM.
The basic idea is to bind an ObservableCollection<string> object to the ItemSource property in your ComboBox and fill it with the items in your text file. I read the file in the constructor of my view model (more on that below) and put all the lines in the Collection (which is bound to the box) on startup. You can also do this elsewhere dynamically if need be.
The usual way to wire it up to the GUI is to use a DataContext in your MainWindow.xaml file. Typically this is done using the Model View ViewModel (MVVM) pattern. The ViewModel is responsible for communicating between the business logic (Model) and the GUI (View). In WPF this is done by having the ViewModel handle event changes in the GUI and also notify the GUI when the data from the model changes. Here I do this with a class called Notifier that implements INotifyPropertyChanged. Then, ViewModel inherits this class and can talk to the GUI via bindings in the xaml. There are other ways of doing the View notifying in WPF/MVVM, but I find this is the simplest. Every WPF project I work on has this class in it.
You will also need bindings for the SelectedItem and SelectedIndex properties in your ComboBox. By binding these to properties that notify in your view model (via calling Update in the setter) you can control all the behavior you need.
Note that I had to remove the StartupUri="MainWindow.xaml" line in the App.xaml file because I instantiate the MainWindow object in the code behind.
Here is how I did it using the standard Visual Stduio WPF application template. Just to clarify how this conforms to MVVM: the 'View' is the GUI itself (defined in MainWindow.xaml), the 'ViewModel' is the ViewModel class, and the 'Model is trivial. It is just a static set of items from the text file to shove into the combobox.
App.xaml:
<Application x:Class="ComboBoxDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ComboBoxDemo">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.cs:
using System.Windows;
namespace ComboBoxDemo
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
ViewModel viewModel = new();
MainWindow mainWindow = new() { DataContext = viewModel };
mainWindow.Show();
}
}
}
MainWindow.xaml:
<Window x:Class="ComboBoxDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboBoxDemo"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:ViewModel}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<ComboBox Grid.Column="0"
ItemsSource="{Binding Items, Mode=OneWay}"
SelectedItem="{Binding SelectedItem, Mode=OneWay}"
SelectedIndex="{Binding ItemIndex}"
Margin="5"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboBoxDemo
{
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent();
}
}
ViewModel.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
namespace ComboBoxDemo
{
public abstract class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged = null;
protected void Update<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChangedEventHandler? handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ViewModel : Notifier
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
// event handling for gui/business logic
// private fields for bound variables
private string _selectedItem = "";
private int _itemIndex = 0;
// properties for binding to GUI
public ObservableCollection<string> Items { get; } = new();
public string SelectedItem
{
get => _selectedItem;
set => Update(ref _selectedItem, value);
}
public int ItemIndex
{
get => _itemIndex;
set
{
Update(ref _itemIndex, value);
// here you can use the index to affect business logic as well
}
}
public ViewModel()
{
Items = new();
string textFile = "items.txt";
using StreamReader file = new StreamReader(textFile);
string? line;
while ((line = file.ReadLine()) is not null)
Items.Add(line);
}
}
}
and my combobox items come from 'items.txt' which I put in the same directory as the .exe:
item1
item2
item3
Screenshot of output:

Passing ModelData (context) between UserControls (views) MVVM Prism

I'm woking on a project and I have three ViewModels: ObjectDetailsViewMode has a Context (property linking to a model) of type ObjectBase; PropertyTextViewModel has a Context of type PropertyText and PropertyNumberViewModel has a Context of type PropertyNumber.
Below is the structure of the Models:
public class ObjectBase : ModelBase
{
private string _name;
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
public DataCollection<PropertyBase> Properties { get; } = new DataCollection<PropertyBase>();
}
public class PropertyText : PropertyBase
{
private string _default;
public string Default
{
get { return _default; }
set { SetProperty(ref _default, value); }
}
}
public class PropertyNumber : PropertyBase
{
private double _default = 0;
public double Default
{
get { return _default; }
set { SetProperty(ref _default, value); }
}
private double _minValue = 0;
public double MinValue
{
get { return _minValue; }
set { SetProperty(ref _minValue, value); }
}
private double _maxValue = 0;
public double MaxValue
{
get { return _maxValue; }
set { SetProperty(ref _maxValue, value); }
}
}
Regarding the views I have one for each ViewModel. The ObjectDetailsView is a use control that has a TextBox for editing the Object.Name, two buttons to add new PropertyText/PropertyNumber to the Object.Properties and an ItemsControl connected to that Object.Properties.
Each PropertyBase in the ItemsControl (ItemsSource) is resolved into a new view using the DataTemplate marker:
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView />
</DataTemplate>
<DataTemplate DataType="{x:Type models:PropertyNumber}">
<views:PropertyNumberView />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
As I'm using PRISM the correct ViewModel is automatically created for me and the view DataContext is then set to the new ViewModel. My problem is I need to pass the new Property from the Object.Properties list to the newly created View's ViewModel and store it in the Context property I have there.
I can't avoid creating a View/ViewModel for each property type because there is some under-the-hood logic on some Property types (not the ones I described here.. but I have other types like Boolean, Reference, Enum...)
So I really need to pass a value to the ViewModel I tried to use
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView Context="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type models:PropertyNumber}">
<views:PropertyNumberView Context="{Binding}"/>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Be aware that Context is a custom property I created inside the ViewModel's to store the ModelContext. I even created a DependencyProperty in the View's behind code:
public PropertyBase Context
{
get { return (PropertyBase)GetValue(ContextProperty); }
set { SetValue(ContextProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContextProperty =
DependencyProperty.Register("Context", typeof(PropertyBase), typeof(PropertyTextView), new PropertyMetadata(null));
But it doesn't get linked to the ViewModels set event (I made a break point there and... nothing). I even tried a SetBinding in the PropertyTextView code-behind (constructor):
string propertyInViewModel = "Context";
var bindingViewMode = new Binding(propertyInViewModel) { Mode = BindingMode.TwoWay };
this.SetBinding(ContextProperty, bindingViewMode);
No luck with any of these... I' really stuck.
Something More Simple
If the PropertyTextView has this dependency property.
public string Context
{
get { return (PropertyBase)GetValue(ContextProperty); }
set { SetValue(ContextProperty, value); }
}
// Using a DependencyProperty as the backing store for Context. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContextProperty =
DependencyProperty.Register("Context", typeof(string), typeof(PropertyTextBuilderView), new PropertyMetadata(null));
I should be able to do:
right?! Why isn't the public property "Context" not being called (I placed a breakpoint there and I get nothing).
Instead of just setting the Context Property of your View to a new Binding you need to assign the Current DataContext like so:
<views:PropertyNumberView Context="{Binding .}"/>
This should assign the Current Views.DataContext Property to your new View.
If you're in an DataTemplate you probably need to specify the RelativeSource:
<views:PropertyNumberView Context="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView Context="{Binding .}"/>
</DataTemplate>
<ItemsControl.Resources>
</ItemsControl>
As I'm using PRISM the correct ViewModel is automatically created for me
You don't have to use view-first with Prism. The ViewModelLocator is there to help, if you chose to, but view model-first is possible, too.
If I understand you correctly, you have a view model and want to populate a list with child view models. So do just that:
internal class ParentViewModel : BindableBase
{
public ParentViewModel( ParentModel parentModel, IChildViewModelFactory factory )
{
Children = new object[] { factory.CreateTextViewModel(parentModel.TextProperty), factory.CreateNumberViewModel(parentModel.NumberProperty) };
}
public IEnumerable Children { get; }
}
and map the different child view models to child views via DataTemplates.
parentModel.WhateverProperty will have a Name and Value properties as well as setter for the value, probably...

WPF Application With Multiple Child Views - MVVM

My problem is quite simple, I want to have a MainView which in turn will have multiple Views which are dynamic and intractable, like in the diagram below:
But to do this you need multiple ViewModels, and I do not know how to organise them.
My original Idea is to have a MainViewModel, within which I will create properties that will return all my ChildViewModels as shown below, but It seems unprofessional to me and a bad practice.
public class MainViewModel : BaseViewModel
{
private EditPropertiesViewModel _editPropertiesViewModel;
public EditPropertiesViewModel EditPropertiesViewModel
{
get { return _editPropertiesViewModel; }
set
{
_editPropertiesViewModel = value;
base.OnPropertyChanged();
}
}
private UsersDetailsViewModel _usersDetailsViewModel;
public UsersDetailsViewModel UsersDetailViewModel
{
get { return _usersDetailsViewModel; }
set
{
_usersDetailsViewModel = value;
base.OnPropertyChanged();
}
}
//etc. etc..
}
Then from My MainView, I would set the Datacontext to the MainViewModel
Please help me I have no idea what to do, I am totally paused right now.
If you wish to achieve this without PRISM you can make use of ContentControl. For every region you create ContentControl and for every ContentControl you create its ViewModel property. Then you manipulate selected ViewModel associated with ContentControl and ContentControl adjusts view based on type of ViewModel assigned. For clarification take a look
XAML:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type viewModel:SubArticleViewModel}">
<view:SubArticleView/>
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding ArticleViewModel}"/>
C#
class BaseViewModel : INotifyPropertyChanged
{
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
class MainViewModel
{
public BaseViewModel ArticleViewModel { get; set; }
}
class SubArticleViewModel : BaseViewModel
{
}
Whenever you assign
ArticleViewModel = new SubArticleViewModel();
DataTemplate defined as resource will be placed as Content of Control.
Above way out creates a lots of work and is more vulnerable for omission. PRISM would be a better choice anyway.
Create AppViewModel class with static ctor like this:
class AppViewModel : BaseViewModel
{
static AppViewModel()
{
_AppModel = new AppViewModel();
}
private static AppViewModel _AppModel;
public static AppViewModel Current
{
get { return _AppModel; }
}
private AppViewModel()
{
//Initialize view models here
MainPageModel = new MainPageViewModel();
}
//VIEW MODELS
public MainPageViewModel MainPageModel { get; private set; }
}
Create BaseViewModel class. All of your VM's should be inherited from it:
class BaseViewModel //implement INotifyPropertyChanged if needed
{
public AppViewModel AppModel
{
get { return AppViewModel.Current; }
}
}
So now you can create UserControl called "MainView":
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
//Prevent view updating in Designer
if (DesignerProperties.GetIsInDesignMode(this))
{
return;
}
var mainVM = AppViewModel.Current.MainPageModel;
DataContext = mainVM;
}
}
In the MainWindow.xaml:
<Window x:Class="MVVM_Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:MVVM_Test.Views"
Title="MainWindow" Height="350" Width="525">
<views:MainView />
</Window>

Add an instance of a class in an attribute via XAML

Hi have a custom class (a Button extended class) where i have a custom property of type Control.
I need this property to access another control an do something.
The property is declared as follow:
private Control mainTab = null;
public Control MainTab { get { return mainTab; } set { mainTab = value; } }
but in XAML when i use:
// "mainTab" is the x:Name of another control in this window
<CustomClass MainTab="mainTab" ....></CustomClass>
i got "Memeber MainTab not recognized or not accessible". Why?
Use a binding to the named element by setting the binding's ElementName property:
<local:CustomClass MainTab="{Binding ElementName=mainTab}" ... />
In order to make this possible, MainTab has to be a dependency property:
public static readonly DependencyProperty MainTabProperty =
DependencyProperty.Register(
"MainTab", typeof(Control), typeof(CustomClass));
public Control MainTab
{
get { return (Control)GetValue(MainTabProperty); }
set { SetValue(MainTabProperty, value); }
}
You have include namespace of your custom class in xaml, then you can use custom class
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Canvas>
<local:CustomClass MainTab=""/>
</Canvas>
and class is
namespace WpfApplication2
{
public class CustomClass
{
public Control MainTab { get; set; }
}
}

Binding to internal ViewModel-Property

I have a UserControl with a ViewModel class as DataContext:
XAML
<UserControl x:Class="DotfuscatorTest.UserControl.View.UserControlView"
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"
mc:Ignorable="d" >
<StackPanel>
<TextBox Text="{Binding ViewModelProperty}"/>
</StackPanel>
</UserControl>
CodeBehind:
namespace DotfuscatorTest.UserControl.View
{
using ViewModel;
public partial class UserControlView
{
public UserControlView()
{
InitializeComponent();
DataContext = new UserControlViewModel();
}
}
}
ViewModel class:
namespace DotfuscatorTest.UserControl.ViewModel
{
public class UserControlViewModel
{
private string viewModelProperty = "hello world";
internal string ViewModelProperty
{
get { return viewModelProperty; }
set { viewModelProperty = value; }
}
}
}
If I set the ViewModelProperty to public the binding works fine. But if I set the property to internal like above the binding fails (Binding error: property not found... ).
I thought an internal property is accessible like public in same assembly. Also I can access to the property from UserControl-codebehind without any problem:
{
...
((UserControlViewModel)DataContext).ViewModelProperty = "hallo viewmodel";
...
Any explenation for this behavior?
Thanks in advance,
rhe1980
As stated here
The properties you use as binding source properties for a binding must
be public properties of your class. Explicitly defined interface
properties cannot be accessed for binding purposes, nor can protected,
private, internal, or virtual properties that have no base
implementation.

Categories