Databinding to dependency properties of my user control doesn't work - c#

I am making a user control to represent chosen numbers (like in a lottery). The problem is that when binding to it inside a data template binding does not work.
It works correclty when hardcoding the values.
The errors are of this type and they appear for every dependency property I bind to
Error: BindingExpression path error: 'BackCheckedColor' property not found on 'NumberControlTest.Controls.NumberControl'. BindingExpression: Path='BackCheckedColor' DataItem='NumberControlTest.Controls.NumberControl'; target element is 'NumberControlTest.Controls.NumberControl' (Name='null'); target property is 'CheckedBackgroundColor' (type 'String')
What I find strange is that in this section of the error
BindingExpression: Path='BackCheckedColor' DataItem='NumberControlTest.Controls.NumberControl'
It suggests that it is trying to find the BackCheckedColor in the usercontrol itself. That does not make sense to me. Can somebody help??
User Control Xaml
<UserControl.Resources>
<local:CheckedToBrushConverter x:Key="CheckedToBrushConverter"
CheckedBackgroundColor="{Binding CheckedBackgroundColor}"
CheckedForegroundColor="{Binding CheckedForegroundColor}"
UncheckedBackgroundColor="{Binding UncheckedBackgroundColor}"
UncheckedForegroundColor="{Binding UncheckedForegroundColor}"/>
</UserControl.Resources>
<Grid Tapped="Grid_Tapped">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16*"/>
<ColumnDefinition Width="130*"/>
<ColumnDefinition Width="16*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="16*"/>
<RowDefinition Height="130*"/>
<RowDefinition Height="30*"/>
</Grid.RowDefinitions>
<Ellipse x:Name="Ellipse" Grid.RowSpan="3" Grid.ColumnSpan="3" Fill="{Binding IsChecked, Converter={StaticResource CheckedToBrushConverter}, ConverterParameter=background}"/>
<Viewbox Grid.Row="1" Grid.Column="1">
<TextBlock x:Name="NumberBlock" TextWrapping="Wrap" FontFamily="Segoe UI" Text="{Binding NumberValue}" Foreground="{Binding IsChecked, Converter={StaticResource CheckedToBrushConverter}, ConverterParameter=foreground}" />
</Viewbox>
</Grid>
User control code behind
public sealed partial class NumberControl : UserControl
{
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this;
}
public string UncheckedBackgroundColor
{
get { return (string)GetValue(UncheckedBackgroundColorProperty); }
set { SetValue(UncheckedBackgroundColorProperty, value); }
}
// Using a DependencyProperty as the backing store for UncheckedBackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty UncheckedBackgroundColorProperty =
DependencyProperty.Register("UncheckedBackgroundColor", typeof(string), typeof(NumberControl), new PropertyMetadata(string.Empty));
public string CheckedBackgroundColor
{
get { return (string)GetValue(CheckedBackgroundColorProperty); }
set { SetValue(CheckedBackgroundColorProperty, value); }
}
// Using a DependencyProperty as the backing store for CheckedBackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CheckedBackgroundColorProperty =
DependencyProperty.Register("CheckedBackgroundColor", typeof(string), typeof(NumberControl), new PropertyMetadata(string.Empty));
plus more dependency properties like those.
MainPage xaml
<Page.Resources>
<DataTemplate x:Key="NumberTemplate">
<Grid>
<controls:NumberControl
UncheckedBackgroundColor="{Binding BackUncheckedColor}"
UncheckedForegroundColor="{Binding ForeUncheckedColor}"
CheckedBackgroundColor="{Binding BackCheckedColor}"
CheckedForegroundColor="{Binding ForeCheckedColor}"
NumberValue="{Binding Value}"
IsChecked="{Binding IsChecked}"
HorizontalAlignment="Center"
VerticalAlignment="Center" Width="45" Height="45"/>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Background="#0f455f">
<GridView x:Name="NumbersGridView" ItemTemplate="{StaticResource NumberTemplate}" ItemsSource="{Binding Numbers, Mode=TwoWay}"/>
<Button x:Name="printButton" Content="Print" VerticalAlignment="Bottom" HorizontalAlignment="Center" Click="printButton_Click"/>
</Grid>
Model class which provides the data of the collection bound to the gridview
public class MockNumber
{
public MockNumber(bool isChecked, int value, string backchcolor, string forchcolor, string backunchcolor, string forunchcolor)
{
IsChecked = isChecked;
Value = value;
BackCheckedColor = backchcolor;
ForeCheckedColor = forchcolor;
BackUncheckedColor = backunchcolor;
ForeUncheckedColor = forunchcolor;
}
public bool IsChecked { get; set; }
public int Value { get; set; }
public string BackCheckedColor { get; set; }
public string ForeCheckedColor { get; set; }
public string BackUncheckedColor { get; set; }
public string ForeUncheckedColor { get; set; }
}
EDIT: How the model is instantiated and bound in the MainPage codebehind.
public MainPage()
{
this.InitializeComponent();
this.DataContext = this;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
makelist();
}
void makelist()
{
for (int i = 1; i <= 20; i++)
{
Numbers.Add(new MockNumber(i % 4 == 0 ? true : false, i, "#dead2b", "#000000", "#dead2b", "#f0b60c"));
}
}
private ObservableCollection<MockNumber> numbers = new ObservableCollection<MockNumber>();
public ObservableCollection<MockNumber> Numbers
{
get
{
return numbers;
}
set
{
numbers = value;
}
}

The reason why it's trying to find the 'BackCheckedColor' property from the NumberControl is because you set the user control's datacontext to itself.
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this;
}
You're telling the user control that your data context is itself. It means that when you do the "{Binding}" the path should be a property of the user control which I don't think is a good idea.
I understand that you want to bind some dependency properties to your Model class but I didn't see in your example where you instantiated the model class and use it as your data context.
Another thing to consider, you might want to use a custom control instead of a user control. I can see that you added some dependency properties to your user control but in practice, dependency properties added to custom controls and static classes that has attached properties.
EDIT:
After reading your additional code, I can see that the user control's datacontext was being set to 'this' which is itself. You need to remove that.
public sealed partial class NumberControl : UserControl
{
public NumberControl()
{
this.InitializeComponent();
this.DataContext = this; //Remove this line
}
//...
Then after removing that, you usercontrol should inherit the GridViewItem's Binding or you can explicitly put the datacontext in your DataTemplate.
<DataTemplate x:Key="NumberTemplate">
<Grid>
<controls:NumberControl DataContext="{Binding}" <!--specify the data context-->
UncheckedBackgroundColor="{Binding BackUncheckedColor}"
//..

Related

WPF MVVM Parent View/ViewModel with child UserControl/ViewModel Data Binding Issue

I have been trying to implement a WPF UserControl with some common functionality between a few different views without success. The UserControl is essentially a ListBox with some Previous & Next buttons and a Search filter. Previous and Next logic is easily copied and pasted, but the filtering is a pain each time, so it would be really nice to encapsulate that all into its own UserControl and ViewModel.
But I've been running into a wall to get the child UserControl/ViewModel to two way bind back to the parent VM.
This works if the child UserControl doesn't have its own ViewModel, but then I have to implement all the functionality in the code behind for that logic, which is unappealing, but not impossible.
I've boiled this down to a demo project- MRE Project - ChildVMBindingDemo
I have a MainWindow, MainWindowViewModel, MyListBoxControl, and a MyListBoxControlViewModel.
The MainWindow.xaml hosts the MyListBoxControl, and forwards two bindings to DependencyProperty in the code behind of the MyListBoxControl. That code behind then forwards those values to the MyListBoxControlViewModel. This is obviously my issue- the "traffic" hits the code behind, sets the values in the child VM, and it's a one way street from there. I've tried every combination of BindingMode, UpdateSourceTrigger, NotifyOnSourceUpdated, and NotifyOnTargetUpdated that I can think of without success.
MainWindow.xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:MyListBoxControl Grid.Column="0"
MyItems="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.MyItems}"
SelectedMyItem="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.SelectedMyItem}"
/>
</Grid>
MainWindow.xaml.cs:
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
this.DataContext = _viewModel;
}
MainWindowViewModel.cs:
public MainWindowViewModel()
{
MyItems = new ObservableCollection<MyItem>()
{
new MyItem() { Name = "One" },
new MyItem() { Name = "Two" },
new MyItem() { Name = "Thee" },
new MyItem() { Name = "Four" },
};
}
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Main View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
MyListBoxControl.xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding MyItems}"
SelectedItem="{Binding SelectedMyItem}"
SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding PrevCommand}"
>Prev</Button>
<Button Grid.Column="2"
Command="{Binding NextCommand}"
>Next</Button>
</Grid>
</Grid>
MyListBoxControl.xaml.cs:
private readonly MyListBoxControlViewModel _viewModel;
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
this.DataContext = _viewModel;
}
public static readonly DependencyProperty MyItemsProperty =
DependencyProperty.Register("MyItems", typeof(ObservableCollection<MyItem>), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, MyItemsChangedCallback));
public ObservableCollection<MyItem> MyItems
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsProperty);
set
{
SetValue(MyItemsProperty, value);
_viewModel.MyItems = MyItems;
}
}
private static void MyItemsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.MyItems = (ObservableCollection<MyItem>)e.NewValue;
}
}
public static readonly DependencyProperty SelectedMyItemProperty =
DependencyProperty.Register(nameof(SelectedMyItem), typeof(MyItem), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, SelectedMyItemChangedCallback)
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set
{
SetValue(SelectedMyItemProperty, value);
_viewModel.SelectedMyItem = SelectedMyItem;
}
}
private static void SelectedMyItemChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.SelectedMyItem = (MyItem)e.NewValue;
}
}
And finally
MyListBoxControlViewModel.cs:
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
private int _selectedIndex;
public int SelectedIndex
{
get => _selectedIndex;
set => Set(ref _selectedIndex, value);
}
private ICommand _prevCommand;
public ICommand PrevCommand => _prevCommand ?? (_prevCommand = new RelayCommand((param) => Prev(), (param) => CanPrev()));
public bool CanPrev() => SelectedIndex > 0;
private void Prev()
{
SelectedIndex--;
}
private ICommand _nextCommand;
public ICommand NextCommand => _nextCommand ?? (_nextCommand = new RelayCommand((param) => Next(), (param) => CanNext()));
public bool CanNext() => MyItems != null ? SelectedIndex < (MyItems.Count - 1) : false;
private void Next()
{
SelectedIndex++;
}
There were preexisting examples similar to this in our project (with the bindings in the code behind passing the values to the child VM)- so someone else struggled with this as well, and it looks like their solution was simply, that the child control never reported back to the parent- they were output only kinda deals.
The only thing I can really think of is to use a Messenger to send the selected value back to the parent directly, or give the child VM an Action to call and set the new value in the code behind dependency properties- but either option just screams of odorous spaghetti, and a probably an endless setter loop/stack overflow exception.
Is there a better approach here, or is there something here that I am just missing?
A control should never depend on an explicit or internal view model. It must depend on its own members, like public properties, alone. Then the data context can later bind to this public properties.
This will enable reusability independent from the actual DataContext type and eliminates redundant code (and redundant complexity) that otherwise would be necessary to delegate values to the private view model.
MVVM does not mean that each control must have its own dedicated view model. It is meant to give the application a structure. MVVM targets application level design and not control level design. A control must implement its UI related logic in its own view code. This can be in code-behind or spread across multiple classes. Such classes would be referenced directly (and not via data binding) as they share the same MVVM context. The MVVM context of UI logic is always View.
Data binding is basically a technology to decouple View and View Model (to allow the View Model to send data to the View without having to reference it - which is crucial to the MVVM pattern).
Data operations usually take place in the View Model (the owner of the data from the View perspective). View would only operate on data views (e.g. to filter or sort collections). But never on data directly.
See how the following example moved all View related logic to the control.
Your fixed and improved (in terms of design) MyListBoxControl, could look as follows:
MyListBoxControl.xaml.cs
public partial class MyListBoxControl : UserControl
{
public static RoutedCommand NextCommand { get; } = new RoutedUICommand("Select next MyItem", "NextCommand", typeof(MyListBoxControl));
public static RoutedCommand PreviousCommand { get; } = new RoutedUICommand("Select previous MyItem", "PreviousCommand", typeof(MyListBoxControl));
public ObservableCollection<MyItem> MyItemsSource
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsSourceProperty);
set => SetValue(MyItemsSourceProperty, value);
}
public static readonly DependencyProperty MyItemsSourceProperty = DependencyProperty.Register(
"MyItemsSource",
typeof(ObservableCollection<MyItem>),
typeof(MyListBoxControl),
new PropertyMetadata(default));
public int SelectedMyItemIndex
{
get => (int)GetValue(SelectedMyItemIndexProperty);
set => SetValue(SelectedMyItemIndexProperty, value);
}
public static readonly DependencyProperty SelectedMyItemIndexProperty = DependencyProperty.Register(
"SelectedMyItemIndex",
typeof(int),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set => SetValue(SelectedMyItemProperty, value);
}
public static readonly DependencyProperty SelectedMyItemProperty = DependencyProperty.Register(
"SelectedMyItem",
typeof(MyItem),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(MyItem), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyListBoxControl()
{
InitializeComponent();
this.CommandBindings.Add(new CommandBinding(NextCommand, ExecuteNextCommand, CanExecuteNextCommand));
this.CommandBindings.Add(new CommandBinding(PreviousCommand, ExecutePreviousCommand, CanExecutePreviousCommand));
}
private void CanExecutePreviousCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex > 0;
private void ExecutePreviousCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Max(this.SelectedMyItemIndex - 1, 0);
private void CanExecuteNextCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex < this.MyItemsSource.Count - 1;
private void ExecuteNextCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Min(this.SelectedMyItemIndex + 1, this.MyItemsSource.Count - 1);
}
MyListBoxControl.xaml
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MyItemsSource}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItem}"
SelectedIndex="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItemIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{x:Static local:MyListBoxControl.PreviousCommand}"
Content="Prev" />
<Button Grid.Column="2"
Command="{x:Static local:MyListBoxControl.NextCommand}"
Content="Next" />
</Grid>
</Grid>
</UserControl>
Usage example
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<MyListBoxControl MyItemsSource="{Binding MyItems}"
SelectedMyItem="{Binding SelectedMyItem}" />
</Window>
In case you meant to add behavior or change the behavior of the existing ListBox, extending ListBox would be the far better option. This would allow to template its items out of the box.
Additionally, if your primary intention was to separate view and related logic, always extend Control i.e. don't create a UserControl. It will also feel more natural to implement the control without a code-behind file. It will also enable more flexibility in terms of customization. For example, although UserControl is a ContentControl, it can't host content.
It's sure not pretty, and it doesn't smell great- but if this is your only option, here is how this could work.
I added an Action to the ViewModel, to set the DP in the code behind- note that it's only calling SetValue, and not directly setting the SelectedMyItem, which prevents the setter loop I was worried about.
MyListBoxControlViewModel.cs
public Action<MyItem> SelectedSetter { get; set; }
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
SelectedSetter?.Invoke(value);
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
and
MyListBoxControl.xmal.cs
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
_viewModel.SelectedSetter = (value) => SetValue(SelectedMyItemProperty, value);
this.DataContext = _viewModel;
}
While not great, it would probably work in limited use.
Probably smart to pass the Action in via the constructor to state its importance in the operation.

WPF ItemsControl data binding with variable path

I am trying to create a UserControl that contains an ItemsControl which should display Parameters and their values. The values must be editable and these values should be transferred back to the ViewModel.
It should be possible to define which property represents the parameter name and which property the parameter value.
Parameter class:
public class Parameter
{
public string Name { get; set; }
public string Value { get; set; }
}
ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
...
public ObservableCollection<Parameter> Parameters { get; set; }
...
}
UserControl ("ParameterList.xaml"):
<UserControl x:Name="ParameterList" ...>
<Border BorderBrush="Black" BorderThickness="1" Height="100">
<!-- I don't know if this binding expression is correct -->
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type controls:ParameterList}}, Path=Parameters}">
<ItemsControl.ItemTemplate>
<Border>
<!-- The property path defined via "ParameterNameMember" should be bound. -->
<TextBlock Text="{Binding ???}" />
<!-- The property path defined via "ParameterValueMember" should be bound. -->
<!-- The value edited in this TextBox should be transferred to the ViewModel. -->
<TextBox Text="{Binding ???}" />
</Border>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
UserControl code behind:
public partial class ParameterList : UserControl
{
public IEnumerable Parameters
{
get => (IEnumerable)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public string ParameterNameMember
{
get => (string)GetValue(ParameterNameMemberProperty);
set => SetValue(ParameterNameMemberProperty, value);
}
public string ParameterValueMember
{
get => (string)GetValue(ParameterValueMemberProperty);
set => SetValue(ParameterValueMemberProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register("Parameters", typeof(object),
typeof(ParameterList), new PropertyMetadata(default(IEnumerable)));
public static readonly DependencyProperty ParameterNameMemberProperty =
DependencyProperty.Register("ParameterNameMember", typeof(string),
typeof(ParameterList), new PropertyMetadata(""));
public static readonly DependencyProperty ParameterValueMemberProperty =
DependencyProperty.Register("ParameterValueMember", typeof(string),
typeof(ParameterList), new PropertyMetadata(""));
public ParameterList()
{
InitializeComponent();
}
}
I want to use the control as follows:
<uc:ParameterList
x:Name="ParameterList"
Parameters="{Binding Parameters}"
ParameterNameMember="Name"
ParameterValueMember="Value" />
Since I don't have that much experience with WPF, I need some help with the data binding. I would be very grateful if I could get some useful suggestions.

WPF binding in data template not working for custom class

Working on a ComboBox that displays a list of available tile backgrounds. This is just a simple ComboBox with an ItemSource set to a collection of MapTileBackground objects.
The MapTileBackground class is defined entirely with properties:
public partial class MapTileBackground
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public byte[] Content { get; set; }
public Nullable<int> Color { get; set; }
public int StrokeColor { get; set; }
public byte StrokeThickness { get; set; }
}
which is defined in a separate library and I would prefer to not change it.
I have defined a simple shape extension to draw the background::
public class MapTileBackgroundPreview : Shape
{
public static readonly DependencyProperty SizeProperty = DependencyProperty.Register("Size", typeof(Point), typeof(MapTileBackgroundPreview));
public static readonly DependencyProperty TileBackgroundProperty = DependencyProperty.Register("TileBackground", typeof(MapTileBackground), typeof(MapTileBackgroundPreview));
public MapTileBackgroundPreview()
{
layout = new Hex.Layout(Hex.Orientation.Flat, new Hex.Point(8, 8), new Hex.Point(4, 4));
Size = new Point(8, 8);
TileBackground = null;
}
private Hex.Layout layout;
protected override Geometry DefiningGeometry
{
get
{
var points = layout.HexCorners(0, 0).ToArray();
var path = new PathFigure();
path.StartPoint = points[5].ToWin();
for (var i = 0; i < 6; i++)
path.Segments.Add(new LineSegment(points[i].ToWin(), true));
var geo = new PathGeometry();
geo.Figures.Add(path);
return geo;
}
}
public Point Size
{
get
{
return (Point)GetValue(SizeProperty);
}
set
{
SetValue(SizeProperty, value);
layout.Size = value.ToHex();
layout.Origin = new Hex.Point(layout.Size.X / 2, layout.Size.Y / 2);
}
}
public MapTileBackground TileBackground
{
get
{
return (MapTileBackground)GetValue(TileBackgroundProperty);
}
set
{
SetValue(TileBackgroundProperty, value);
if (value == null)
{
Fill = Brushes.Transparent;
Stroke = Brushes.Black;
StrokeThickness = 1;
}
else
{
Stroke = value.Stroke();
StrokeThickness = value.StrokeThickness();
Fill = value.Fill(layout.Orientation);
}
}
}
}
The layout is just a conversion utility between screen pixel coordinates and a hexagonal system. DefiningGeometry just add 6 line segments of the hex. The TileBackground setter, when given a not null MapTileBackground, updates the Stroke and Fill as the background defines. I've tested this control successfully (outside the combo box data template).
And speaking of:
<DataTemplate x:Key="TileListItemRenderer">
<Grid Width="225">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackground="{Binding /}"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
So I just create a shape, and two labels, bind the shape to the current MapTileBackground object (combo box ItemSource is a collection of MapTileBackground objects), and the labels to Name and Description.
My problem is the shape is always drawn empty (as in TileBackground is null) and the setter is never invoked. Both the Name Label and Description TextBlock behave as expected (display correct text). And during my debugging attempts, I created an id property on the preview object which in turn invokes the TileBackground Setter and bound it to the Id property (avoid current object bind), again, the TileBackgroundId setter is never invoked. I even added a new label bound to Id to see if that was working and it displays the id as expected. Here are those changes that again did not work. The TileBackgroundId or TileBackground properties are never set when opening the drop down.
<DataTemplate x:Key="TileListItemRenderer">
<Grid Width="225">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="75"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackgroundId="{Binding Id}"/>
<Label Grid.Row="0" Grid.Column="0" Content="{Binding Id}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview));
public int TileBackgroundId
{
get
{
return (int)GetValue(TileBackgroundIdProperty);
}
set
{
SetValue(TileBackgroundIdProperty, value);
TileBackground = TMapTileBackgroundTool.Get(value);
}
}
TMapTileBackgroundTool.Get() returns the correct object based on Id.
I have also tested instances of MapTileBackgroundPreview setting TileBackgroundId outside the data template.
Any thoughts as to what is going on?
The setter of the CLR wrapper for the dependency property is not supposed to be set as the WPF binding engine calls the GetValue and SetValue methods directly:
Setters not run on Dependency Properties?
Why are .NET property wrappers bypassed at runtime when setting dependency properties in XAML?
The getter and setter of the CLR wrapper property should only call the GetValue and SetValue method respectively.
If you want to do something when the dependency property is set, you should register a callback:
public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview),
new PropertyMetadata(0, new PropertyChangedCallback(TileBackgroundIdChanged)));
public int TileBackgroundId
{
get
{
return (int)GetValue(TileBackgroundIdProperty);
}
set
{
SetValue(TileBackgroundIdProperty, value);
}
}
private static void TileBackgroundIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MapTileBackgroundPreview ctrl = (MapTileBackgroundPreview)d;
ctrl.TileBackground = TMapTileBackgroundTool.Get((int)e.NewValue);
}

setting the DataContex correctly

im building a UserControl MyUserControl that has his own ViewModel MyUserControlViewModel. MyUserControl contains 6 VehicleSelectionBlock (V1, ... V6). VehicleSelectionBlock is a UserControl i've made. it has 3 RadioButton: car, train, bus; all are of enum type Vehicle and of the same GroupName VehicleGroup.
my goal is to represent each of MyUserControl's VehicleSelectionBlocks in MyUserControlViewModel.
to make my self clear: in MyUserControlViewModel i want to be able to know&change what RadioButton is checked in every one of the 6 VehicleSelectionBlock. i think my main problem is not the converter but rather the DataContex - i'm not sure how to set it correctly for each of the controllers.
iv'e tried Binding (which is the obvious solution). i tried reading here, here , and here. unfortunately neither one helped my acheive my goal.
my code is below - im kinda new to wpf and data binding in generally. i've read almost every chapter in this tutorial but still lost sometimes.
please help me get through this and understand better the DataContex concept.
ty
MyUserContlor.xaml.cs:
namespace Project01
{
/// <summary>
/// Interaction logic for MyUserContlor.xaml
/// </summary>
public partial class MyUserContlor : UserControl
{
public MyUserContlorViewModel ViewModel { get; set; }
public MyUserContlor()
{
ViewModel = new MyUserContlorViewModel();
InitializeComponent();
this.DataContext = ViewModel;
}
private void BtnImReady_OnClick(object sender, RoutedEventArgs e)
{
//this code is irrelevant to the question
throw NotImplementedException();
}
}
}
MyUserContlor.xaml:
<UserControl x:Class="Project01.MyUserContlor"
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:loc="clr-namespace:Project01"
mc:Ignorable="d"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center">
<Viewbox Stretch="Uniform">
<StackPanel>
<loc:VehicleSelectionBlock Name="V1"/>
<loc:VehicleSelectionBlock Name="V2"/>
<loc:VehicleSelectionBlock Name="V3"/>
<loc:VehicleSelectionBlock Name="V4"/>
<loc:VehicleSelectionBlock Name="V5"/>
<loc:VehicleSelectionBlock Name="V6"/>
<Button x:Name="BtnImReady" Click="BtnImReady_OnClick">Im Ready!</Button>
</StackPanel>
</Viewbox>
</UserControl>
MyUserContlorViewModel.cs:
namespace Project01
{
public class MyUserContlorViewModel : INotifyPropertyChanged
{
public MyUserContlorViewModel()
{
VehicleArr = new MyViewModel_Vehicle[6];
PropertyChanged+=MyUserControlViewModel_PropertyChanged;
}
public MyViewModel_Vehicle[] VehicleArr;
public event PropertyChangedEventHandler PropertyChanged;
public PropertyChangedEventHandler GetPropertyChangedEventHandler() { return PropertyChanged; }
private void MyUserControlViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//might be useful
throw NotImplementedException();
}
}
//this class should represent a VehicleSelectionBlock
public class MyViewModel_Vehicle
{
public Vehicle VehicleSelected {get; set;}
MyViewModel_Vehicle(){}
MyViewModel_Vehicle(Vehicle v){ VehicleSelected = v;}
}
}
VehicleSelectionBlock.xaml:
<UserControl x:Class="Project01.VehicleSelectionBlock"
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:Project01"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}">
<Border VerticalAlignment="Center" HorizontalAlignment="Center" Background="GhostWhite"
BorderBrush="Gainsboro" BorderThickness="1">
<StackPanel >
<Label Content="{Binding Name}"
FontWeight="Bold" HorizontalContentAlignment="Center"></Label>
<RadioButton GroupName="VehicleGroup" >car</RadioButton>
<RadioButton GroupName="VehicleGroup">train</RadioButton>
<RadioButton GroupName="VehicleGroup" IsChecked="True">bus</RadioButton>
</StackPanel>
</Border>
</Grid>
</UserControl>
VehicleSelectionBlock.xaml.cs:
namespace Project01
{
/// <summary>
/// Interaction logic for VehicleSelectionBlock.xaml
/// </summary>
public partial class VehicleSelectionBlock : UserControl
{
public VehicleSelectionBlock()
{
InitializeComponent();
}
public VehicleSelectionBlock(String name)
{
name = Name;
InitializeComponent();
}
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
"Name", typeof (String), typeof (VehicleSelectionBlock), new PropertyMetadata(default(String)));
public String Name
{
get { return (String) GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
}
public enum Vehicle { Car, Train, Bus}
}
here is a quick solution. keep in mind that the code needs to change if you want to add more values to your Vehicle enum.
the MyUserControlViewModel.cs file
public class MyUserControlViewModel
{
public MyUserControlViewModel()
{
VehicleArr = new VehicleViewModel[6];
for (int i = 0; i < 6;i++ )
VehicleArr[i] = new VehicleViewModel();
}
public VehicleViewModel[] VehicleArr { get; set; }
}
this will expose your 6 items. They could be more. As a result they will be displayed in an ItemsControl, as you will see later.
public class VehicleViewModel:ViewModelBase
{
private bool isCar, isTrain, isBus;
public bool IsCar
{
get { return isCar; }
set
{
if (isCar == value) return;
isCar = value;
OnChanged("IsCar");
}
}
public bool IsTrain
{
get { return isTrain; }
set
{
if (isTrain == value) return;
isTrain = value;
OnChanged("IsTrain");
}
}
public bool IsBus
{
get { return isBus; }
set
{
if (isBus == value) return;
isBus = value;
OnChanged("IsBus");
}
}
}
instances of VehicleViewModel will contain your radio selection using 3 bool properties. this is the solution disadvantage. If you want more values you'll have to add more properties. you can see this inherits ViewModelBase. ViewModelBase just implements INPC so i'm not going to put it here. ViewModelBase also exposes the OnChange method that triggers the INPC event.
displaying the list can be done in your MyUserControl by using an ItemsControl like below.
<ItemsControl ItemsSource="{Binding VehicleArr}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<loc:VehicleControl />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
each item is also a UserControl. The VehicleControl user control is just a StackPanel that displays the RadioButons. This can be seen below.
<StackPanel Orientation="Horizontal">
<RadioButton Content="Car" Margin="5" VerticalAlignment="Center" IsChecked="{Binding Path=IsCar, Mode=TwoWay}"/>
<RadioButton Content="Train" Margin="5" VerticalAlignment="Center" IsChecked="{Binding Path=IsTrain, Mode=TwoWay}"/>
<RadioButton Content="Bus" Margin="5" VerticalAlignment="Center" IsChecked="{Binding Path=IsBus, Mode=TwoWay}"/>
</StackPanel>
please notice that each RadioButton is bound to one of the 3 properties in the VehicleViewModel instance.
Once you press your button you should have all the selections recorded. if you want you could have a function that returns an enum value by analysing the 3 bool properties if that is what you need.
the best solution will be to get rid of the radio buttons and replace them with combo boxes. in this way you can change the enum members and everything will continue to work without changing anything else. this might look as below.
public class VehicleViewModel:ViewModelBase
{
private Vehicle selOption;
private readonly Vehicle[] options;
public VehicleViewModel()
{
this.options = (Vehicle[])Enum.GetValues(typeof(Vehicle));
}
public Vehicle[] Options { get { return options; } }
public Vehicle SelectedOption
{
get { return selOption; }
set
{
if (selOption == value) return;
selOption = value;
OnChanged("SelectedOption");
}
}
}
and for the view:
<ItemsControl ItemsSource="{Binding VehicleArr}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Options}"
SelectedItem="{Binding SelectedOption, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can do directly in the code-behind of your control (in the default constructor)
public VehicleSelectionBlock()
{
InitializeComponent();
this.DataContext = new MyUserContlorViewModel ();
}
You can also do that in XAML (http://msdn.microsoft.com/en-us/library/ms746695(v=vs.110).aspx) declaration, as you wish.

Custom UserControl with ContentControl field

I have a UserControl which acts as a wrapper for a ContentControl, which is simply a title to the ContentControl.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Background="Green" Grid.Row="0">
<TextBlock Text="{Binding Header}" Style="{StaticResource HeaderStyle}" Margin="12, 10, 0, 10" />
</Grid>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Content="{Binding Body}" Grid.Row="1"/>
</Grid>
And here's where I try to use the control:
<gbl:ListHeader Grid.Row="1" Visibility="{Binding HasMovies, Converter={StaticResource VisibilityConverter}}" Header="{Binding Path=LocalizedResources.movie_list_header, Source={StaticResource LocalizedStrings}}" >
<gbl:ListHeader.Body>
<ListBox SelectionChanged="ListBoxContainerSelectionChanged" ItemsSource="{Binding Movies}" ItemContainerStyle="{StaticResource HeaderListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<gbl:MovieItemControl Header="{Binding MovieTitle}" Description="{Binding FormattedDescription}" Detail="{Binding FormattedDetail}" Opacity="{Binding IsSuppressed, Converter={StaticResource DimIfTrueConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</gbl:ListHeader.Body>
The DataBinding to the list happens, however nothing displays in the control. I'm guessing that it's still there, but too small to see (undefined h/w).
Is there something that I'm doing wrong? The header shows fine, so the control appears to be working somewhat.
Edit:
Here's the code-behind for ListHeader:
public partial class ListHeader : UserControl
{
private readonly ListHeaderData _data = new ListHeaderData();
public ListHeader()
{
InitializeComponent();
DataContext = _data;
}
public string Header
{
get { return (string)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
// Using a DependencyProperty as the backing store for Header. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(string), typeof(ListHeader), new PropertyMetadata("",HeaderPropertyChanged) );
private static void HeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var lh = d as ListHeader;
if (lh != null)
lh._data.Header = e.NewValue as string;
}
public object Body
{
get { return GetValue(BodyProperty); }
set { SetValue(BodyProperty, value); }
}
// Using a DependencyProperty as the backing store for Body. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BodyProperty =
DependencyProperty.Register("Body", typeof(object), typeof(ListHeader), new PropertyMetadata(null, BodyPropertyChanged));
private static void BodyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var lh = d as ListHeader;
if (lh != null)
lh._data.Body = e.NewValue;
}
}
public class ListHeaderData : ViewModelBase
{
public ListHeaderData()
{
if (IsInDesignMode)
{
Header = "Custom Header Goes Here";
Body = new Grid() { Background = new SolidColorBrush(Colors.Yellow) };
}
}
private string _header;
public string Header
{
get { return _header; }
set { _header = value; RaisePropertyChanged("Header"); }
}
private object _body;
public object Body
{
get { return _body; }
set { _body = value; RaisePropertyChanged("Body");}
}
}
In addition to what i said in my comment you appear to bind to your DataContext in the UserControl declaration which is a Bad Thing and the problem of all this.
You appear to want to bind to the properties of the UserControl but you bind directly to the properties of the DataContext which is your ViewModel, hence setting the Body property on an instance in XAML does nothing as the property is sidestepped by the internal binding.
UserControls should for all i know do bindings like this:
<UserControl Name="control" ...>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Background="Green" Grid.Row="0">
<TextBlock Text="{Binding Header, ElementName=control}" Style="{StaticResource HeaderStyle}" Margin="12, 10, 0, 10" />
</Grid>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Content="{Binding Body, ElementName=control}" Grid.Row="1"/>
</Grid>
Get rid of those dependency property changed callbacks and change the property code in ViewModels to this format to make sure it changed:
private int _MyProperty = 0;
public int MyProperty
{
get { return _MyProperty; }
set
{
if (_MyProperty != value)
{
_MyProperty = value;
OnPropertyChanged("MyProperty");
}
}
}

Categories

Resources