Binding to Nested Element in Attached Property in WPF - c#

I am trying to bind an element nested inside of an attached property to my DataContext, but the problem is that the attached property is not part of the logical tree and therefore does not properly set or bind to the data context of the parent object. The dependency property, in this case Value, is always null.
Here is some example XAML
<StackPanel>
<!-- attached property of static class DataManager -->
<local:DataManager.Identifiers>
<local:TextIdentifier Value="{Binding Path=MyViewModelString}" />
<local:NumericIdentifier Value="{Binding Path=MyViewModelInt}" />
<local:NumericIdentifier Value="{Binding Path=SomeOtherInt}" />
</local:DataIdentifiers>
<!-- normal StackPanel items -->
<Button />
<Button />
</StackPanel>
Due to the implementation, this cannot be a single attached property - it needs to be a collection that allows for n entities. Another acceptable solution would be to put the identifiers directly in the node, but I don't think this syntax is possible without including these element explicitly in the logical tree. i.e...
<Button>
<local:NumericIdentifier Value="{Binding}" />
<local:TextIdentifier Value="{Binding}" />
<TextBlock>Actual button content</TextBlock>
</Button>
Here is the start of the implementation of DataManager.
[ContentProperty("IdentifiersProperty")]
public static class DataManager
{
public static Collection<Identifier> GetIdentifiers(DependencyObject obj)
{
return (Collection<Identifier>)obj.GetValue(IdentifiersProperty);
}
public static void SetIdentifiers(DependencyObject obj, Collection<Identifier> value)
{
obj.SetValue(IdentifiersProperty, value);
}
public static readonly DependencyProperty IdentifiersProperty =
DependencyProperty.RegisterAttached("Identifiers", typeof(Collection<Identifier>), typeof(DataManager), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIdentifiersChanged)));
}
I've tried making the base class Identifiers implement Freezable in the hopes that it would for the inheritance of the data and binding context, but that did not have any effect (likely because it is nested inside another layer - the attached property).
A couple more key points:
I would like this to work on any UIElement, not just StackPanels
The Identifiers are not part of the visual tree. They do not and should not have visual elements
as this is an internal library, I would prefer to avoid requiring a Source or RelativeSource to the binding as it is not intuitive that this needs to be done
Is it possible to bind to the inherited DataContext in this layer of the markup? Do I need to manually add these to the logical tree? If so, how?
Thanks!

In addition to having Identifier inherit from Freezable, you will need to also use FreezableCollection instead of Collection<Identifier> as attached property type. This will ensure that inheritance chain is not broken.
public class Identifier : Freezable
{
... // dependency properties
protected override Freezable CreateInstanceCore()
{
return new Identifier();
}
}
Create a custom collection:
public class IdentifierCollection : FreezableCollection<Identifier> { }
And, modify attached property to use this collection:
[ContentProperty("IdentifiersProperty")]
public static class DataManager
{
public static readonly DependencyProperty IdentifiersProperty =
DependencyProperty.RegisterAttached(
"Identifiers",
typeof(IdentifierCollection),
typeof(DataManager),
new FrameworkPropertyMetadata(OnIdentifiersChanged));
...
public static void SetIdentifiers(UIElement element, IdentifierCollection value)
{
element.SetValue(IdentifiersProperty, value);
}
public static IdentifierCollection GetIdentifiers(UIElement element)
{
return element.GetValue(IdentifiersProperty) as IdentifierCollection;
}
}
Sample usage:
<Window.DataContext>
<local:TestViewModel
MyViewModelInt="123"
MyViewModelString="Test string"
SomeOtherInt="345" />
</Window.DataContext>
<StackPanel x:Name="ParentPanel" ... >
<!-- attached property of static class DataManager -->
<local:DataManager.Identifiers>
<local:IdentifierCollection>
<local:TextIdentifier Value="{Binding Path=MyViewModelString}" />
<local:NumericIdentifier Value="{Binding Path=MyViewModelInt}" />
<local:NumericIdentifier Value="{Binding Path=SomeOtherInt}" />
</local:IdentifierCollection>
</local:DataManager.Identifiers>
<!-- normal StackPanel items -->
<TextBlock Text="{Binding Path=(local:DataManager.Identifiers)[0].Value,
ElementName=ParentPanel, StringFormat=Identifer [0]: {0}}" />
<TextBlock Text="{Binding Path=(local:DataManager.Identifiers)[1].Value,
ElementName=ParentPanel, StringFormat=Identifer [1]: {0}}" />
<TextBlock Text="{Binding Path=(local:DataManager.Identifiers)[2].Value,
ElementName=ParentPanel, StringFormat=Identifer [2]: {0}}" />
</StackPanel>

Related

Catel: multiple ViewModels with one View. Is it possible?

I got a generic base-class for ViewModel of my user-control:
public class SuggestModule<TEntity> : ViewModelBase
where TEntity : class, ISuggestable, new()
{
public SuggestModule(ISomeService someService)
{
// Some logic
}
// Some private fields, public properties, commands, etc...
}
}
Whitch has many inheritable classes. That is two of them, for example:
public class CitizenshipSuggestViewModel : SuggestModule<Citizenship>
{
public CitizenshipSuggestViewModel(ISomeService someService)
: base(someService) { }
}
public class PlaceOfBirthSuggestViewModel : SuggestModule<PlaceOfBirth>
{
public PlaceOfBirthSuggestViewModel(ISomeService someService)
: base(someService) { }
}
That is view implementation:
<catel:UserControl
x:Class="WPF.PRC.PBF.Views.UserControls.SuggestUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://schemas.catelproject.com"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:pbf="clr-namespace:WPF.PRC.PBF">
<Grid>
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"> />
<ListBox ItemsSource="{Binding ItemsCollection}" />
// Other elements, behaviors, other extensive logic...
</Grid>
</catel:UserControl>
Now, in MainWindow creating two ContentControls:
<catel:Window
x:Class="WPF.PRC.PBF.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://schemas.catelproject.com">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" Content="{Binding CitizenshipSuggestViewModel, Converter={catel:ViewModelToViewConverter}}" />
<ContentControl Grid.Row="1" Content="{Binding PlaceOfBirthSuggestViewModel, Converter={catel:ViewModelToViewConverter}}" />
</Grid>
</catel:Window>
Due to violation of the Naming Convention, manually resolving a ViewModel in App.xaml.cs:
var viewModelLocator = ServiceLocator.Default.ResolveType<IViewModelLocator>();
viewModelLocator.Register(typeof(SuggestUserControl), typeof(CitizenshipSuggestViewModel));
viewModelLocator.Register(typeof(SuggestUserControl), typeof(PlaceOfBirthSuggestViewModel));
var viewLocator = ServiceLocator.Default.ResolveType<IViewLocator>();
viewLocator.Register(typeof(CitizenshipSuggestViewModel), typeof(SuggestUserControl));
viewLocator.Register(typeof(PlaceOfBirthSuggestViewModel), typeof(SuggestUserControl));
But now I have two views with identical ViewModels.
How can I solve this problem without creation of identical Views with repetition of the code in each of them?
Thank you in advance!
You have a few options:
Repeat yourself so if one needs customization in the future, you can customize just 1 without much overhead. The downside is that if you need to check the generic behavior, you'll need to change them all.
Create an enum representing the state that a VM is representing. In that case, you can simply creat a single vm that catches all of the cases that you need to handle. You can solve this using a dependency property on the view and by using ViewToViewModelMapping to automatically map this to the vm. This comes closest to achieving code-reusage as you wish to achieve with your view. It does go a bit against "separation of concerns", but since it represents the same sort of data I believe it's still a good approach.
For 2, you need to do the following:
1 Create an enum SuggestEntityType with PlaceOfBirth, Citizenship, etc
2 Create a property on the vm (this example code is assuming you are using Catel.Fody):
public SuggestedEntityType EntityType { get; set; }
3 Create a dependency property on the view:
[ViewToViewModel(MappingType = ViewToViewModelMappingType.ViewToViewModel)]
public SuggestedEntityType EntityType
{
get { return (SuggestedEntityType) GetValue(EntityTypeProperty); }
set { SetValue(EntityTypeProperty, value); }
}
public static readonly DependencyProperty EntityTypeProperty = DependencyProperty.Register("EntityType", typeof (SuggestedEntityType),
typeof (MyControl), new PropertyMetadata(null));
4 You can now use the user control like this:
<controls:MyView EntityType="Citizenship" />
For more info, see http://docs.catelproject.com/vnext/catel-mvvm/view-models/mapping-properties-from-view-to-view-model/
One possibility is to create a dependencyProperty in your UserControl codebehind, for example
#region Properties
public string Test
{
get { return (string)GetValue(TestProperty); }
set { SetValue(TestProperty, value); }
}
#endregion Properties
#region Dependency Properties
public static readonly System.Windows.DependencyProperty TestProperty =
System.Windows.DependencyProperty.Register("Test", typeof(string), typeof(YourUserControl), new System.Windows.FrameworkPropertyMetadata() { BindsTwoWayByDefault = true });
#endregion Dependency Properties
Then in your xaml you can bind this property as :
<TextBlock Text="{Binding Test, RelativeSource={RelativeSource AncestorType={x:Type catel:UserControl}, Mode=FindAncestor}}">
Then in your MainWindow you can write:
<views:YourUserControlName Test="{Binding SomeTextPropertyFromMainWindowVM}"/>
So you will be able to bind from the property SomeTextPropertyFromMainWindowVM in your windowVM to some property in your userControl.
If you have in your main window several viewModels, you can write like:
<views:YourUserControlName Test="{Binding SomeViewModel.SomeTextProperty}"/>
<views:YourUserControlName Test="{Binding SomeOtherViewModel.SomeTextProperty}"/>

Creating generalized user controls with MVVM Light

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>

Custom TabItem properties (TabItem.Content and TabItem.Header)

I'm trying to inherit MyTabItem from System.Windows.Controls.TabItem class. The problem is, that original TabItem has properties of generic object type:
public object Header;
public object Content;
I'm trying to hide those properties in my derived class with different types.
public class MyTabItem: TabItem
{
public new MyTabHeader Header;
public new MyTabContent Content;
}
This way, I can access MyTabItem.Header and MyTabItem.Content without type casting.
The idea is pretty decent and code compiles correctly. However when application starts I see empty controls (no error is reported). When I remove those lines and use base class properties, it works fine.
Of course I could add two additional properties which would internally return casted (MyTabHeader)Header or (MyTabContent)Header, but it seems to be a little redundant.
I'm asking if there is any other way to correctly implement those properties, so they actually work in my application.
This runs completely counter to how WPF was designed to be used. Your XAML objects are supposed to be loosely bound to data, in the vast majority of cases you shouldn't even need to create a custom control. The fact that you are doing this, and then trying to replace the members with type-safe versions of your own, means your view code and your view logic code are no longer separated, and that is going to create you a world of headache down the track.
If you need dynamic tabbing then one way to do it is to first declare an abstract class representing your pages and to derive your page types from it:
public interface IBasePage
{
string Header { get; }
}
public class MyPageA : ViewModelBase, IBasePage
{
public string Header { get { return "Page A"; } }
}
public class MyPageB : ViewModelBase, IBasePage
{
public string Header { get {return "Page B";} }
}
public class MyPageC : ViewModelBase, IBasePage
{
public string Header { get {return "Page C";} }
}
Your view model (which is what your window DataContext should be set to) should then contain a collection of the tabbed pages you wish to display:
public class MyViewModel : ViewModelBase
{
private IEnumerable<IBasePage> _MyPages = new List<IBasePage>(){
new MyPageA(),
new MyPageB(),
new MyPageC()
};
public IEnumerable<IBasePage> MyPages {get {return this._MyPages;}}
}
The tab control in your XAML is then loosely bound to this and should contain a style for your TabItem (so it know what text to use for the header etc) and DataTemplates so that it knows how to render each of the page types you've created:
<TabControl ItemsSource="{Binding MyPages}" SelectedItem="{Binding MyPages[0], Mode=OneTime}">
<TabControl.Resources>
<!-- TabItem style -->
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Header}" />
</Style>
<!-- Content templates -->
<DataTemplate DataType="{x:Type local:MyPageA}">
<TextBlock Text="This is page A" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageB}">
<TextBlock Text="This is page B" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyPageC}">
<TextBlock Text="This is page C" />
</DataTemplate>
</TabControl.Resources>
</TabControl>
The end result is a regular tab control that is completely data driven and is bound to your already-strongly-typed models:

Behavior of DependencyProperty With Regards to Multiple FrameworkElement Instances

So I tried using DependencyProperty to solve my issues with regards to passing the local ViewModel across child Views. However a question popped in my head.
For example I need to make multiple instances of a certain FrameworkElement, say, a UserControl. That UserControl has a DependencyProperty defined. As stated in books, a dependency property instance should be static and readonly. How would the DependencyProperty work in that kind of scenario? Would it work the same way as a conventional UserControl property, or whatever object instance you pass to the DependencyProperty, it'll be passed across all instances of the said UserControl?
Yes, it will operate as a normal property. If you need a property for a specific control, that is one property for a single control, you can use just dependency property. They will be passed through all the instances of the class. But if you want the property on many controls, then should use the attached dependency property, which will be available to all members within a certain namespace. Properties, such as: Canvas.Top, DockPanel.Dock are attached DependencyProperty.
Sample of attached dependency properties:
public class MyDependencyClass : DependencyObject
{
public static readonly DependencyProperty IsSelectedProperty;
public static void SetIsSelected(DependencyObject DepObject, Boolean value)
{
DepObject.SetValue(IsSelectedProperty, value);
}
public static Boolean GetIsSelected(DependencyObject DepObject)
{
return (Boolean)DepObject.GetValue(IsSelectedProperty);
}
private static bool IsSelectedValid(object Value)
{
if (Value.GetType() == typeof(bool))
{
return true;
}
else
{
return false;
}
}
static MyDependencyClass()
{
FrameworkPropertyMetadata MetaData = new FrameworkPropertyMetadata((Boolean)false);
IsSelectedProperty = DependencyProperty.RegisterAttached("IsSelected",
typeof(Boolean),
typeof(MyDependencyClass),
MetaData,
new ValidateValueCallback(IsSelectedValid));
}
}
They also contain useful callback's like OnPropertyChangedCallback, ValidateValueCallback which can be placed in an additional logic.
These properties are also available in XAML. Add "local" namespace:
xmlns:local="clr-namespace:SampleApp"
Define for element's:
<Button Name="Button1" local:MyDependencyClass.IsSelected="True" />
<Button Name="Button2" local:MyDependencyClass.IsSelected="False" />
...
<ListBoxItem Name="Sample" local:MyDependencyClass.IsSelected="True" />
Access to property in triggers:
<Trigger Property="local:MyDependencyClass.IsSelected" Value="True">
<Setter Property="Background" Value="Green" />
</Trigger>
Work with attached dependency properties in code:
if (CurrentButtonName == MyButton.Name)
{
MyDependencyClass.SetIsSelected(CurrentButton, true);
}
else
{
MyDependencyClass.SetIsSelected(CurrentButton, false);
}
For more info see: http://msdn.microsoft.com/en-us/library/ms749011.aspx

WPF binding to property of non-simple property of other class

A little bit can't figure out how to use WPF binding in this case:
Assume, we have an object Car with non-simple property of type CarInfo:
public class CarInfo : DependencyObject
{
public static readonly DependencyProperty MaxSpeedProperty =
DependencyProperty.Register("MaxSpeed", typeof (double), typeof (CarInfo), new PropertyMetadata(0.0));
public double MaxSpeed
{
get { return (double) GetValue(MaxSpeedProperty); }
set { SetValue(MaxSpeedProperty, value); }
}
}
public class Car : DependencyObject
{
public static readonly DependencyProperty InfoProperty =
DependencyProperty.Register("Info", typeof (CarInfo), typeof (Car), new PropertyMetadata(null));
public CarInfo Info
{
get { return (CarInfo) GetValue(InfoProperty); }
set { SetValue(InfoProperty, value); }
}
}
Also assume, Car is an ui element and it has the Car.xaml, something simple:
<Style TargetType="assembly:Car">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="assembly:Car">
<Grid >
!--> <TextBlock Text="{Binding Path=MaxSpeed}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
So, I wanted this TextBlock, in my Car.xaml, to represent the property "MaxSpeed" of my CarInfo class, which is actually a property of my Car class. How can I do this?
Thank you in advance, appreciate any help! :)
It depends upon what is assigned to the DataCOntext of the UI element representing the Car - you need to specify a binding path relative to that. In this case I would suggest you start with this:
<TextBlock Text="{Binding Path=Info.MaxSpeed}" />
this is assuming that a Car object has been assigned to the DataContext of the Car UI element.
Note that your properties don't have to be dependency properties - you can also bind to normal properties (depending on what you are doing).
Edit
It seems you are looking to use element binding, so you should be able to achieve what you want by using either the TemplatedParent or an ancestor as your relative source. See this previous SO answer for an example. Your binding should look something like this:
<TextBlock Text="{Binding Path=Info.MaxSpeed, RelativeSource={RelativeSource TemplatedParent}}" />
This will take you back to your templated parent control (the Car), then travel down the Info property of the UI element to the MaxSpeed property on its contents.
As I said in my comment, you are making this very messy by having your UI element so closely match your data element and then assigning your data object to a relatively non standard property on the UI element. You might have your reasons, but XAML and WPF don't need to be that complicated.
<TextBlock Text="{Binding Path=Info.MaxSpeed}" />
That code works well for me:
<TextBlock Text="{Binding Path=Info.MaxSpeed, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
and usage:
Car.Info = new CarInfo { MaxSpeed = 100.0 };

Categories