I'm trying to create a UserControl in my WPF project which I want should have a DependencyProperty that I can bind to in the parent. The project is written as MVVM and I'm using Caliburn micro.
I really want to write clean and maintainable code using MVVM, so I want my UserControls to utilize viewmodels as much as possible and code behind as little as possible.
The problem is that I'm unsuccessful in getting the binding between the parent and the UserControl viewmodel to work correctly.
MyUserControl:
public partial class MyUserControlView : UserControl
{
public MyUserControlView()
{
InitializeComponent();
// If no Datacontext is set, binding between parent property and textbox text works - one way only (set from parent)!.
// -
// If Datacontext is set to this, bindings with properties in MyUserControlView code behind works.
//DataContext = this;
// If Datacontext is set to MyUserControlViewModel, binding between MyUserControlViewModel and MyUserControlView works, but not with parent.
DataContext = new MyUserControlViewModel();
}
public string ProjectNumber
{
get { return (string)GetValue(MyUserControlValueProperty); }
set { SetValue(MyUserControlValueProperty, value); }
}
public static readonly DependencyProperty MyUserControlValueProperty =
DependencyProperty.Register("ProjectNumber", typeof(string), typeof(MyUserControlView), new PropertyMetadata(null, new PropertyChangedCallback(OnProjectNumberUpdate)));
private static void OnProjectNumberUpdate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var view = d as MyUserControlView;
view.ProjectNumberText.Text = e.NewValue as string;
}
}
MyUserControl code behind:
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="In MyUserControl: " />
<TextBlock Text="{Binding ProjectNumber}" />
</StackPanel>
<TextBox Name="ProjectNumberText" Text="{Binding ProjectNumber, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
</StackPanel>
MyUserControl ViewModel:
public class MyUserControlViewModel : Screen
{
private string _projectNumber;
public string ProjectNumber
{
get { return _projectNumber; }
set
{
_projectNumber = value;
NotifyOfPropertyChange(() => ProjectNumber);
}
}
}
Parent view:
<StackPanel>
<local:MyUserControlView ProjectNumber="{Binding ParentProjectNumber}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="In parent: "/>
<TextBlock Text="{Binding ParentProjectNumber}" />
</StackPanel>
</StackPanel>
Parent ViewModel:
public class ShellViewModel : Screen
{
public ShellViewModel()
{
ParentProjectNumber = "Hello from parent!";
}
private string _parentProjectNumber;
public string ParentProjectNumber
{
get { return _parentProjectNumber; }
set
{
_parentProjectNumber = value;
NotifyOfPropertyChange(() => ParentProjectNumber);
}
}
}
I know I'm probably way off here, but I have no idea what to do to get the bindings to work correctly.
Is there a better way to bind between a DependencyProperty and a viewmodel? Can I put the DP in the viewmodel somehow?
Here is the entire project solution: https://github.com/ottosson/DependencyPropertyTest
don't change UserControl.DataContext from inside UserControl. it can and will create issues later.
use proper name for DP (ProjectNumberProperty and corresponding ProjectNumber) and add BindsTwoWayByDefault to metadata:
public partial class MyUserControlView : UserControl
{
public MyUserControlView()
{
InitializeComponent();
}
public string ProjectNumber
{
get { return (string)GetValue(ProjectNumberProperty); }
set { SetValue(ProjectNumberProperty, value); }
}
public static readonly DependencyProperty ProjectNumberProperty = DependencyProperty.Register
(
"ProjectNumber",
typeof(string),
typeof(MyUserControlView),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)
);
}
fix bindings in xaml:
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="In MyUserControl: " />
<TextBlock Text="{Binding Path=ProjectNumber, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
<TextBox Text="{Binding Path=ProjectNumber, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
that should do it.
btw, "clean and maintainable code using MVVM" and "want my UserControls to utilize viewmodels as much as possible" sort of contradict each other.
also nothing wrong with code-behind in UserControls as long as that code handles only view functionality. for example: DataGrid source code contains 8000+ LoC
Related
I have an ItemsControl that should display the values of some properties of an object.
The ItemsSource of the ItemsControl is an object with two properties: Instance and PropertyName.
What I am trying to do is displaying all the property values of the Instance object, but I do not find a way to set the Path of the binding to the PropertyName value:
<ItemsControl ItemsSource={Binding Path=InstanceProperties}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=PropertyName, Mode=OneWay}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Source=??{Binding Path=Instance}??, Path=??PropertyName??, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
the question marks are the points where I don't know how to create the binding.
I initially tried with a MultiValueConverter:
<TextBlock Grid.Column="1" Text="{Binding}">
<TextBlock.DataContext>
<MultiBinding Converter="{StaticResource getPropertyValue}">
<Binding Path="Instance" Mode="OneWay"/>
<Binding Path="PropertyName" Mode="OneWay"/>
</MultiBinding>
</TextBlock.DataContext>
</TextBlock>
The MultiValueConverter uses Reflection to look through the Instance and returns the value of the property.
But if the property value changes, this change is not notified and the displayed value remains unchanged.
I am looking for a way to do it with XAML only, if possible, if not I will have to write a wrapper class to for the items of the ItemsSource collection, and I know how to do it, but, since it will be a recurring task in my project, it will be quite expensive.
Edit:
For those who asked, InstanceProperties is a property on the ViewModel which exposes a collection of objects like this:
public class InstanceProperty : INotifyPropertyChanged
{
//[.... INotifyPropertyChanged implementation ....]
public INotifyPropertyChanged Instance { get; set; }
public string PropertyName { get; set; }
}
Obviously the two properties notify theirs value is changing through INotifyPropertyChanged, I don't include the OnPropertyChanged event handling for simplicity.
The collection is populated with a limited set of properties which I must present to the user, and I can't use a PropertyGrid because I need to filter the properties that I have to show, and these properties must be presented in a graphically richer way.
Thanks
Ok, thanks to #GazTheDestroyer comment:
#GazTheDestroyer wrote: I cannot think of any way to dynamically iterate and bind to an arbitrary object's properties in XAML only. You need to write a VM or behaviour to do this so you can watch for change notifications, but do it in a generic way using reflection you can just reuse it throughout your project
I found a solution: editing the ViewModel class InstanceProperty like this
added a PropertyValue property
listen to PropertyChanged event on Instance and when the PropertyName value changed is fired, raise PropertyChanged on PropertyValue
When Instance or PropertyName changes, save a reference to Reflection's PropertyInfo that will be used by PropertyValue to read the value
here is the new, complete, ViewModel class:
public class InstanceProperty : INotifyPropertyChanged
{
#region Properties and events
public event PropertyChangedEventHandler PropertyChanged;
private INotifyPropertyChanged FInstance = null;
public INotifyPropertyChanged Instance
{
get { return this.FInstance; }
set
{
if (this.FInstance != null) this.FInstance.PropertyChanged -= Instance_PropertyChanged;
this.FInstance = value;
if (this.FInstance != null) this.FInstance.PropertyChanged += Instance_PropertyChanged;
this.CheckProperty();
}
}
private string FPropertyName = null;
public string PropertyName
{
get { return this.FPropertyName; }
set
{
this.FPropertyName = value;
this.CheckProperty();
}
}
private System.Reflection.PropertyInfo Property = null;
public object PropertyValue
{
get { return this.Property?.GetValue(this.Instance, null); }
}
#endregion
#region Private methods
private void CheckProperty()
{
if (this.Instance == null || string.IsNullOrEmpty(this.PropertyName))
{
this.Property = null;
}
else
{
this.Property = this.Instance.GetType().GetProperty(this.PropertyName);
}
this.RaisePropertyChanged(nameof(PropertyValue));
}
private void Instance_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == this.PropertyName)
{
this.RaisePropertyChanged(nameof(PropertyValue));
}
}
private void RaisePropertyChanged(string propertyname)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion
}
and here is the XAML:
<ItemsControl ItemsSource={Binding Path=InstanceProperties}>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=PropertyName, Mode=OneWay}"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Path=PropertyValue, Mode=OneWay}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have this Custom Control
XAML:
<UserControl x:Class="WpfApplication1.UC"
...
x:Name="uc">
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Horizontal">
<TextBox Text="{Binding Test, ElementName=uc}" Width="50" HorizontalAlignment="Left"/>
</StackPanel>
</UserControl>
C#
public partial class UC : UserControl
{
public static readonly DependencyProperty TestProperty;
public string Test
{
get
{
return (string)GetValue(TestProperty);
}
set
{
SetValue(TestProperty, value);
}
}
static UC()
{
TestProperty = DependencyProperty.Register("Test",typeof(string),
typeof(UC), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
public UC()
{
InitializeComponent();
}
}
And this is how i used that custom control:
<DockPanel>
<ItemsControl ItemsSource="{Binding Path=DataList}"
DockPanel.Dock="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" CommandParameter="{Binding}" Click="Button_Click"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<local:UC Test="{Binding SelectedString, Mode=OneWay}"/>
</DockPanel>
--
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
private ObservableCollection<string> _dataList;
public ObservableCollection<string> DataList
{
get { return _dataList; }
set
{
_dataList = value;
OnPropertyChanged("DataList");
}
}
private string _selectedString;
public string SelectedString
{
get { return _selectedString; }
set
{
_selectedString = value;
OnPropertyChanged("SelectedString");
}
}
public MainWindow()
{
InitializeComponent();
this.DataList = new ObservableCollection<string>();
this.DataList.Add("1111");
this.DataList.Add("2222");
this.DataList.Add("3333");
this.DataList.Add("4444");
this.DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.SelectedString = (sender as Button).CommandParameter.ToString();
}
}
If I do not change text of UC, everything is ok. When I click each button in the left panel, button's content is displayed on UC.
But when I change text of UC (ex: to 9999), Test property lost binding. When I click each button in the left panel, text of UC is the same that was changed (9999). In debug I see that SelectedString is changed by each button click but UC's text is not.
I can 'fix' this problem by using this <TextBox Text="{Binding Test, ElementName=uc, Mode=OneWay}" Width="50" HorizontalAlignment="Left"/> in the UC.
But I just want to understand the problem, can someone help me to explain it please.
Setting the value of the target of a OneWay binding clears the binding. The binding <TextBox Text="{Binding Test, ElementName=uc}" is two way, and when the text changes it updates the Test property as well. But the Test property is the Target of a OneWay binding, and that binding is cleared.
Your 'fix' works because as a OneWay binding, it never updates Test and the binding is never cleared. Depending on what you want, you could also change the UC binding to <local:UC Test="{Binding SelectedString, Mode=TwoWay}"/> Two Way bindings are not cleared when the source or target is updated through another method.
The issue is with below line
<local:UC Test="{Binding SelectedString, Mode=OneWay}"/>
The mode is set as oneway for SelectString binding so text will be updated when the value from code base changes. To change either the source property or the target property to automatically update the binding source as TwoWay.
<local:UC Test="{Binding SelectedString, Mode=TwoWay}"/>
I'm working on a WPF MVVM application. I'm looking to databind a WebBrowser control to a view model which is in turn bound to a Tab. Following the advice in this article, I created a static helper class consisting of a static DependancyProperty:
public static class WebBrowserHelper
{
public static readonly DependencyProperty BodyProperty =
DependencyProperty.RegisterAttached("Body", typeof(string), typeof(WebBrowserHelper), new PropertyMetadata(OnBodyChanged));
public static string GetBody(DependencyObject dependencyObject)
{
return (string)dependencyObject.GetValue(BodyProperty);
}
public static void SetBody(DependencyObject dependencyObject, string body)
{
dependencyObject.SetValue(BodyProperty, body);
}
private static void OnBodyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
string newValue = (string)e.NewValue;
var webBrowser = (WebBrowser)d;
webBrowser.NavigateToString(newValue);
}
}
XAML Binding WebBrowser to DependancyProperty:
<WebBrowser Grid.Column="2" HorizontalAlignment="Center" src:WebBrowserHelper.Body="{Binding HTMLBody}" VerticalAlignment="Center" Height="Auto" Width="Auto" />
ViewModel that bound to ItemsSource of Tab Control:
public class SomeVM : ViewModelBase, INotifyPropertyChanged
{
private string _htmlBody;
private SomeView _myView = new SomeView();
public SomeVM (string tabName)
{
TabName = tabName;
string contentsAsHTML = do_a_whole_bunch_of_stuff_to_generate_an_HTML_string();
HTMLBody = contentsAsHTML;
}
public string HTMLBody
{
get { return _htmlBody; }
set
{
if (_htmlBody != value)
{
_htmlBody = value;
RaisePropertyChanged("HTMLBody");
}
}
}
public SomeView View
{
get {return _myView;}
set { }
}
public string TabName { get; set; }
}
MainViewModel, Creating the Tab collection:
private ObservableCollection<SomeVM> _tabs;
public ObservableCollection<SomeVM> Tabs
{
get
{
if (_tabs== null)
{
_tabs= new ObservableCollection<SomeVM>();
_tabs.Add(new SomeVM("Tab 1"));
_tabs.Add(new SomeVM("Tab 2"));
_tabs.Add(new SomeVM("Tab 3"));
}
return _tabs;
}
}
MainWindow.xaml setting up the Tab Binding:
<TabControl ItemsSource="{Binding Tabs, Source={StaticResource vm}}"
>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding TabName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding View}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My problem is that "OnBodyChanged" is fired multiple times on ever tab change. The HTML takes a few seconds to load, and I would rather it only loads when the property is actually modified in the viewmodel.
EDIT
Here's the smallest sample project that recreates my problem.
Your problem is not relevant to attached properties or MVVM.
In fact, the real problem is that TabControl destroy and recreate its child every time you change the selected tab. That would explain why the handler is invoked more than once. The VisualTree only contains the selected Tab.
If you can try with another control, you will see there are no errors.
For solving this issue, I will redirect you to this post.
I have areally wierd problem when i'm using some simple custom control i've built:
this is the custom control code :
public partial class ToolButton : Button
{
public string ToolID
{
get { return (string)GetValue(ToolIDProperty); }
set { SetValue(ToolIDProperty, value); }
}
public static readonly DependencyProperty ToolIDProperty =
DependencyProperty.Register("ToolID", typeof(string), typeof(ToolButton), new UIPropertyMetadata(""));
public ToolButton()
{
InitializeComponent();
}
}
Now when i'm trying to ude this custom control in the main window like that :
<ItemsControl Margin="100" ItemsSource="{Binding Path=Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<customControls:ToolButton Height="100" Width="100" Margin="10" Content="{Binding Value.Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And the binding doesn't work !!
but when i'm using simple button the binding works excellent..
is someone faced similliar problem ??
Thanks...
put the following line into your ToolButton's constructor:
this.DataContext = this;
I am having trouble getting the following scenario to work (this code is not the actual code but the principals are the same. Basically I need to pass a value down from a MainPage down to a nested "reusable user control" that binds to it's own properties. I want to see the "This is it!" text on the screen but it's not being set in the SilverlightControl2 control (I suspect due to the setting of the DataContext) - but I how do I fix it?
MainPage.xaml
<Grid>
<ContentPresenter>
<ContentPresenter.Content>
<Local:SilverlightControl1 OneValue="This is it!"/>
</ContentPresenter.Content>
</ContentPresenter>
</Grid>
SilverlightControl1.xaml
<Grid>
<Local:SilverlightControl2 TwoValue="{Binding OneValue}"/>
</Grid>
SilverlightControl1.xaml.cs
public partial class SilverlightControl1 : UserControl
{
public string OneValue
{
get { return (string)GetValue(OneValueProperty); }
set { SetValue(OneValueProperty, value); }
}
public static readonly DependencyProperty OneValueProperty = DependencyProperty.Register(
"OneValue", typeof(string), typeof(SilverlightControl1), new PropertyMetadata(string.Empty));
public SilverlightControl1()
{
InitializeComponent();
this.DataContext = this;
}
}
SilverlightControl2.xaml
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="{Binding TwoValue}" Foreground="Blue" />
</Grid>
SilverlightControl2.xaml.cs
public partial class SilverlightControl2 : UserControl
{
public string TwoValue
{
get { return (string)GetValue(TwoValueProperty); }
set { SetValue(TwoValueProperty, value); }
}
public static readonly DependencyProperty TwoValueProperty = DependencyProperty.Register(
"TwoValue", typeof(string), typeof(SilverlightControl2), new PropertyMetadata(string.Empty));
public SilverlightControl2()
{
InitializeComponent();
this.DataContext = this;
}
}
As soon as you find yourself feeling the need to do this:-
this.DataContext = this;
know that you have probably got things wrong. Its probably the first thing I would expect to find on Silverlight specific "bad smell list".
In this case where you are specialising UserControl a better approach is to do this:-
SilverlightControl1.xaml
<Grid>
<Local:SilverlightControl2 x:Name="MyControl2" />
</Grid>
SilverlightControl1.xaml.cs (I'm just showing the constructor the rest is as you have it)
public SilverlightControl1()
{
InitializeComponent();
MyControl2.SetBinding(SilverlightControl2.TwoValueProperty , new Binding("OneValue") { Source = this });
}
SilverlightControl2.xaml
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock x:Name="MyTextBox" Foreground="Blue" />
</Grid>
SilverlightControl1.xaml.cs (I'm just showing the constructor the rest is as you have it)
public SilverlightControl2()
{
InitializeComponent();
MyTextBox.SetBinding(TextBox.TextProperty , new Binding("TwoValue") { Source = this });
}
Since in UserControls you know the structure of the XAML and you can name the elements that you need access to in code, you can create the binding using a line of code instead.
This leaves the DataContext free to do what is designed for rather than be hi-jacked for a different purpose.
The alternative approach where instead of specialising UserControl you create a templated control, in this case the binding can be expressed in XAML using something like:-
<TextBox Text="{TemplateBinding TwoValue}" />
Template binding only works in ControlTemplate so you can't use it in a UserControl.