XAML DependencyProperty object notify parent on new instance - c#

I have DependecyProperty (named Block) on a XAML UserControl (named BlockPresenter) which expects a data type of IBlock. This IBlock intefae has a Visual GetControl() method - amongst other things - which returns an element of some kind to allow the user to edit the values for that specific block. The BlockPresenter provides a way of choosing which block to use (e.g. BlockA, BlockB, etc.) and also shows the GetControl return valued for the block itself.
There are models which contain one or more IBlock properties under varying different names. Each of these models has their own user control which has the model as it's DataContext. In the XAML for the model's control, I have lines such as <controls:BlockPresenter Block="{Binding Foo}" /> to show the control and allow the user to change the block type or the properties for their chosen block.
When the user changes block type from the dropdown in BlockPresenter, a new instance of the selected block is created and set as the Block DependencyProperty and the presenter calls GetControl for the new Block. This is fine, however the problem is that this does not send the new value up to the model.
BlockPresenter.xaml
<Border BorderThickness="2" BorderBrush="#F00">
<StackPanel>
<ComboBox x:Name="BlockSelection" SelectedValuePath="BlockType" DisplayMemberPath="Name" SelectionChanged="BlockSelection_SelectionChanged" />
<ContentControl x:Name="ContentContainer" Margin="0" />
</StackPanel>
</Border>
BlockPresenter.xaml.cs
public BlockPresenter() {
InitializeComponent();
BlockSelection.ItemsSource = new List<BlockListItem> {
new BlockListItem("BlockA", typeof(BlockA)),
new BlockListItem("BlockB", typeof(BlockB)),
// BlockListItem is a simple class containing just "Name" and "BlockType" as read-only auto properties.
};
}
private static void OnBlockChange(DependencyObject conditionPresenter, DependencyPropertyChangedEventArgs eventArgs) {
var control = (BlockPresenter)conditionPresenter;
var newBlock = (IBlock)eventArgs.NewValue;
control.ContentContainer.Content = newBlock?.GetControl();
control.BlockSelection.SelectedValue = eventArgs.NewValue?.GetType();
}
public static readonly DependencyProperty BlockProperty = DependencyProperty.Register("Block", typeof(IBlock), typeof(BlockPresenter), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnBlockChange));
public IBlock Block {
get => (IBlock)GetValue(BlockProperty);
set => SetValue(BlockProperty, value);
}
private void BlockSelection_SelectionChanged(object sender, SelectionChangedEventArgs e) {
Block = (IBlock)Activator.CreateInstance(BlockSelection.SelectedValue);
}
DefaultModelControl.xaml
<Grid>
<Grid.ColumnDefinitions>
...
</Grid.ColumnDefitions>
<Label Content="Main Block:" Grid.Row="0" />
<logic:BlockPresenter Block="{Binding MainBlock}" Grid.Row="0" Grid.Column="1" />
<Label Content="Subblock:" Grid.Row="1" />
<logic:BlockPresenter Block="{Binding SubBlock}" Grid.Row="1" Grid.Column="1" />
</Grid>
When the main or sub block types are changed for the default model, the value inside the model (MainBlock or SubBlock properties) are not updated to reflect the newly selected block.
I know one solution could be to raise a custom event (e.g. BlockChanged) from the BlockPresenter when a new value is set, passing this value as event args and then capturing this event in the DefaultModelControl.xaml.cs file and setting the property to the new reference.
I was wondering if there is another way of doing this without the listeners though, since it's annoying to have to create them for each block presenter I want.
Am I going about this wrong? Would love to hear peoples constructive thoughts on this. Thanks :)

Related

C# & wpf - Unexpected behavior of (OneWay-Mode) chain-binding between ListBox-Label-ComboBox

I have the following strange (for me) situation
A ListBox is bound (as Source) to a Label with OneWay Mode, i.e. ListBox is read-only.
The Label is then bound to a ComboBox with TwoWay binding
ListBox --> Label <--> ComboBox - arrows denote binding mode
The strange thing is that when the program starts and the user selects through the list in the ListBox, all 3 controls behave as expected.
But as soon as one index is chosen from Combobox, the Label continues to work properly (is updated by the Combo), but the OneWay binding to ListBox disappears (is null) and the ListBox cannot update the Label any more.
It seems to me that when Label Content is set by other means besides the OneWay binding (as here with the Combo updating or maybe with a ValueConverter), this binding is cleared by WPF.
The other strange behavior is that if this OneWay binding between ListBox and Label is turned into a TwoWay one, then everything works perfectly.
The question is what am I doing wrong, or if this is the normal behavior, where could I find relevant documentation.
Please find below simplified code and XAML demonstrating the case.
My workaround is to set the Label Content with code in ListBox_SelectionChanged.
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Test_Chained_controls
{
public partial class MainWindow : Window
{
public class ComboItems
{
public int iDX { get; set; }
public string sDesc { get; set; }
public ComboItems(int a, string b)
{
iDX = a;
sDesc = b;
}
}
public class ListItems
{
public int iLDX { get; set; }
public ListItems(int a)
{
iLDX = a;
}
}
public List<ListItems> intList = new List<ListItems>();
public List<ComboItems> idx_StrList = new List<ComboItems>();
public MainWindow()
{
InitializeComponent();
intList.Add(new ListItems(0));
intList.Add(new ListItems(1));
intList.Add(new ListItems(2));
intList.Add(new ListItems(3));
idx_StrList.Add(new ComboItems(0, "Zero"));
idx_StrList.Add(new ComboItems(1, "One"));
idx_StrList.Add(new ComboItems(2, "Two"));
idx_StrList.Add(new ComboItems(3, "Three"));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
listBox.ItemsSource = intList;
comboBox.ItemsSource = idx_StrList;
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//// Set Label Content in case of OneWay
// var binding = BindingOperations.GetBinding(label, Label.ContentProperty);
// if (binding != null)
// {
// if (binding.Mode == BindingMode.OneWay)
// {} // Binding set - do nothing
// }
// else label.Content = listBox.SelectedItem;
}
}
}
XAML
<Window ... normal stuff
xmlns:local="clr-namespace:Test_Chained_controls"
mc:Ignorable="d"
Title="MainWindow" Height="182" Width="500" Loaded="Window_Loaded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="100"/>
</Grid.RowDefinitions>
<Label Content="ListBox" Grid.Row="0" Grid.Column="0" Margin="20,10,0,0" />
<Label Content="Label" Grid.Row="0" Grid.Column="1" Margin="20,10,0,0" />
<Label Content="ComboBox" Grid.Row="0" Grid.Column="2" Margin="20,10,0,0" />
<ListBox x:Name="listBox" Grid.Row="1" Grid.Column="0" Margin="0"
DisplayMemberPath="iLDX"
SelectedIndex="0"
IsSynchronizedWithCurrentItem="True"
SelectionChanged="ListBox_SelectionChanged"/>
<Border BorderThickness="1" Grid.Row="1" Grid.Column="1" Height="30"
Margin="20,20,0,0" BorderBrush="#FFACACAC" >
<!-- *********** Label with Mode=OneWay or TwoWay *********** -->
<Label x:Name="label" Width="100" Height="25"
Content="{Binding ElementName=listBox,
Path=SelectedItem.iLDX, Mode=OneWay }" />
</Border>
<ComboBox x:Name="comboBox" Grid.Row="1" Grid.Column="2"
Height="30" Margin="20,20,0,0"
DisplayMemberPath="sDesc"
SelectedValue="{Binding ElementName=label, Path=Content,
TargetNullValue=0, FallbackValue=0, Mode=TwoWay}"
SelectedValuePath="iDX" />
</Grid>
</Window>
EDIT
Relevant documentation: Dependency properties overview
Local value: A local value might be set through the convenience of the property wrapper, which also equates to setting as an attribute or property element in XAML, or by a call to the SetValue method using a property of a specific instance. If you set a local value by using a binding or a static resource, these each act in the precedence as if a local value was set, and bindings or resource references are erased if a new local value is set.
and further down
If you set another local value for a property that originally held a Binding value, you will overwrite the binding entirely, not just the binding's run-time value.
As I understand, there was some kind of bug related to this case, fixed with the introduction of DependencyObject.SetCurrentValue The Control Local Values Bug Solution
public void SetCurrentValue (System.Windows.DependencyProperty dp, object value);
// Sets the value of a dependency property without changing its value source.
It seems to me that Combobox TwoWay binding is still using SetValue, and that's why the binding for (label) gets erased when my (combobox) is used.
To overcome this, I changed the TwoWay binding of (comboBox) to OneWay, and entered the following in the comboBox_DropDownClosed event (showing the currently selected Item), in order to update (label) by code without erasing the existing binding
private void comboBox_DropDownClosed(object sender, System.EventArgs e)
{
Binding binding = BindingOperations.GetBinding(label, Label.ContentProperty);
if (binding != null)
{
ComboItems ComboItem = comboBox.SelectedItem as ComboItems;
int iDX = ComboItem.iDX;
// Set label value without affecting existing binding
label.SetCurrentValue(Label.ContentProperty, iDX);
}
}
With the use of SetCurrentValue, code works now as originally intended by "simulating" the TwoWay mode.
There's nothing strange at all. Data binding is designed to work this way. When you assign a binding to a dependency property, it means you change the local value of this dependency property to a binding expression. And any update provide by the binding source will be the effective value of this dependency property. If the binding is working in one way mode, any update to this dependency property from other then binding source, will overwrite the local value, result in losing the binding. On the other side, becuase two mode is suppose to update the binding source, dependency object will count any non-expression value as effective value, binding will keep working until you replace or clear it.
DependencyObject.GetValue gets the effective value.
DependencyObject.ReadLocalValue gets the local value.
DependencyObject.SetValue sets the local value.
DependencyObject.SetCurrentValue sets the effective value.
DependencyObject.ClearValue clears the local value.

Instantiating a new UserControl programmatically while setting a DataContext to facilitate binding

Starting with the MahApps-Material mashup demo, I'm trying to use a button click event to create a new TabItem from my Views. For now, the CustomTabItem will show text bound to some property from a FancyObject (being served to the View from my FancyTabViewModel). But I've got the DataContext, dependency property or the Binding done wrong.
public void NewTabOnClick(object sender, RoutedEventArgs e)
{
// create my new object from Models
FancyObject fo = new FancyObject();
// create the INofify VM and pass it my object
// the VM has a public VMFancyObject property to serve the fo
FancyTabViewModel fvm = new FancyTabViewModel(fo);
// create the new UserControl and set its context to the VM
CustomTabItem newTab = new CustomTabItem() {
Header = "New tab"
};
newTab.DataContext = fvm;
MainWindowTabs.Items.Add(newTab);
}
And in my <TabItem x:Class="MyProject.Views.CustomTabItem" there is a label so bound: <Label Content="{Binding VMFancyObject.SomeList.Count}"/>
I expect to see the default count of the List created in the FancyObject's constructor. However, after the new tab is created and added to the dragablz:TabablzControl, I just see a blank label.
I also tried <Label DataContext="{Binding Path=DataContext.FancyTabViewModel,RelativeSource={RelativeSource AncestorType={x:Type TabItem}}}" Content="{Binding VMFancyObject.SomeList.Count}" HorizontalAlignment="Left" Margin="341,196,0,0" Foreground="Black" Background="#FF97FF02"/>

Pass Pivot.SelectedIndex to user control in Windows 10 universal app

I have a user control and a pivot on the same page.
I would like the user control to show something different based on the selected tab in the pivot.
At first, I thought that I could pass the pivot.SelectedIndex to the user control as a dependency property like so:
// Pivot property
public static readonly DependencyProperty SelectedTabProperty = DependencyProperty.Register
(
"SelectedTab",
typeof(int),
typeof(GaugeControl),
new PropertyMetadata(-1)
);
public int SelectedTab
{
get {
return (int)GetValue(SelectedTabProperty); }
set
{
SetValue(SelectedTabProperty, value);
}
}
With xaml in the parent page:
<local:GaugeControl
Data="{x:Bind data_collector}"
SelectedTab="{Binding ElementName=MainPivot, Path=SelectedIndex}"
Margin="0,0,0,100"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<Pivot x:Name="MainPivot"
HorizontalAlignment="Center"
Margin="0,0,0,0"
Style="{StaticResource PivotStyleBottomHeader}">
....
However, this code generates an error The application called an interface that was marshalled for a different thread in the getter.
Is there a better way to do this?

Keep a reference to objects passed to a UserControl

I created a UserControl that has a ContentControl in it. This ContentControl gets Buttons from the normal .xaml-pages. But depending on some events I need to change this Button's Label or Image but i am getting a NullReferenceException.
UserControl1.xaml
<Grid>
<!-- different Stuff that needs to be around -->
<ContentControl Content="{Binding UserControlContent, ElementName=userContent}"/>
</Grid>
UserControl1.xaml.cs
public static readonly DependencyProperty AppBarContentProperty =
DependencyProperty.Register("UserControlContent", typeof(Grid), typeof(UserControl1), new PropertyMetadata(new Grid()));
public Grid UserControlContent
{
get { return (Grid)GetValue(UserControlContentProperty); }
set { SetValue(UserControlContentProperty, value); }
}
MainPage.xaml
<local:UserControl1>
<local:UserControl1.UserControlContent>
<Grid>
<Controls:RoundButton x:Name="btn1"/>
</Grid>
</local:UserControl1.UserControlContent>
</local:UserControl1>
MainPage.xaml.cs
MainPage()
{
btn1.Label = "new label";
}
As soon as I try this with a button inside of the UserControl it fails. With buttons that stay outside it works.
Is there any deeper binding possible to keep control of these buttons?
The trick is using the mvvm-binding!
The button's values are bound now:
Label="{Binding RoundButtons[3].Label}"
Visibility="{Binding RoundButtons[3].VisibilityState, FallbackValue=Visible}"
This allows me to define default-values and still change them on the fly as I need them to be changed.
Hope someone needs this information ;)

WP7.8: Bound items in scrollbox updated with wrong data

Overview
I have an application, that displays data from an observable collection. The observable collection is (in this debugging setting) created and instanciated only once, then the values stay the same.
The main view of the application contains a ListBox that is bound to said observable collection:
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel MinWidth="456" MaxWidth="456" Background="White" Margin="0,0,0,17">
<sparklrControls:SparklrText Post="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Workaround used to stretch the child elements to the full width -> HorizontalContentAlignment won't work for some reason... -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The child items are bound to a UserControl. This UserControl implements a DependancyProperty which the child elements are bound to:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(object), new PropertyMetadata(textPropertyChanged));
private static void postPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SparklrText control = d as SparklrText;
control.Post = (ItemViewModel)e.NewValue;
}
Binding to the post property configures other variables via the getter of the Post property
public ItemViewModel Post
{
get
{
return post;
}
set
{
if (post != value)
{
this.ImageLocation = value.ImageUrl;
this.Username = value.From;
this.Comments = value.CommentCount;
this.Likes = value.LikesCount;
this.Text = value.Message;
post = value;
}
}
}
This setter configures other which in turn set up elements in the user control. Nothing in the user control is bound, the few updates are done with direct access to the respective Content/Text properties. ImageLocation performs an asynchronous download of an image with
private void loadImage(string value)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
image = new BitmapImage();
image.SetSource(e.Result);
MessageImage.Source = image;
};
wc.OpenReadAsync(new Uri(value));
}
Issue
When I scroll down in the list box and back up, the setter of Post is executed when the owning element comes back into view. The problem: value is a different instance of ItemViewModel. The ListBox ItemsSource is not accessed in any way from outside the class. When scrolling back up, it seems like the wrong Items are bound to the elements, resulting in distorted designs. Are there any issues with the Binding that cause this?
The issue was caused by the ListBox. Elements that are scroll out of view are recycled and appended on the other side. In the code above, a asynchronous operation did not check if the result was still valid, causing wrong display data.

Categories