Can a DependencyProperty bound two times to a TwoWay binding? - c#

I have a Control that displays something (let's call it Display). In this control I have a class Camera that stores things like zoom, position and rotation.
I can change the zoom from an external control (let's call it ZoomBar).
Now I had the idea to connect all of them with a TwoWay-Binding like this:
ZoomBar.Value <--> Display.Zoom <--> Camera.Zoom
It should be like: ZoomBar value changes --> update Display.Zoom --> update Camera.Zoom. Display.Zoom does not really do something. It's only for exchange the data between Camera and ZoomBar.
But I get nothing. After a short check in the Camera:
public float Zoom
{
get { MessageBox.Show("Any calls here?"); return (float)GetValue(ZoomProperty); }
set { ... }
}
I get a massive amount of MessageBoxes. I guess there is something like loop in there. Like ZoomBar.Value --> Display.Zoom --> ZoomBar.Value --> ...
My question
Are the two-way bindings causing the problem and if it is the bindings, is there a XAML way to fix this?
XAML ZoomBar
<StatusBarItem Title="Zoom Bar" HorizontalAlignment="Right">
<Slider x:Name="uxInputZoom" Style="{DynamicResource ZoomSliderStyle}" Value="100" Maximum="500" Minimum="20" />
</StatusBarItem>
XAML Display
<Display x:Name="uxDisplay" Zoom="{Binding Value, Converter={StaticResource PercentToFractionConverter}, ElementName=uxInputZoom, Mode=TwoWay}" />
Code Display
public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register("Zoom", typeof(float), typeof(Display), new FrameworkPropertyMetadata(1f));
public float Zoom
{
get { return (float)GetValue(ZoomProperty); }
set { SetValue(ZoomProperty, value); }
}
Camera _camera = new Camera();
//...
public Display()
{
Binding binding = new Binding("Zoom");
binding.Source = _camera;
binding.Mode = BindingMode.TwoWay;
BindingOperations.SetBinding(this, Display.ZoomProperty, binding);
InitializeComponent();
...
}

You certainly can data bind one property value to more than one UI control. Take this simple example which enables movements of the Slider to update the value in the TextBox, while also enabling values entered in the TextBox to update the Slider.Value property:
<StackPanel>
<Slider Value="{Binding Width2}" Minimum="0.0" Maximum="100.0" Margin="0,0,0,20" />
<TextBox Text="{Binding Width2, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
This will not cause any feedback loops as in your code, so I suspect that you have something else doing that.

Related

How WPF Moving GUI Binding

I am writing this because I had difficulties in implementing a specific function during WPF implementation.
For data model
latitude and longitude (location in current window)
MainViewModel is
I am managing it as an observableCollection.
In xaml, the upper and longitude values ​​for each model list were displayed,
The part that implements the button control in the form of an image to move according to the location information that is continuously updated in xaml is blocked, so I am posting this.
In addition, I want to display a line for the azimuth or each component, but I also want to implement this so that the line moves according to the changing value.
Is there a method that is usually used for these changing values ​​or is there a method that is mainly used in practice?
In the case of Winform, I used the method of drawing a line using a Graphics object, but if anyone knows how to display it in real time by moving it in real time in C# WPF and binding the position value, I would appreciate it if you could share it.
In my previous answer I demonstrated a way to position a line based on a property in the viewmodel.
However, this only answers part of your question.
In the example below I will demonstrate how to have an ObservableCollection with positions that will be reflected on screen by showing the positions as text at the screen location corresponding with their values.
To start with, while the ObservableCollection can be used as an Itemsource, we must make sure the Items themselves are suitable to be used as viewmodels:
So, you could declare a Positionclass as follows (using 'CommunityToolkit.Mvvm'):
using CommunityToolkit.Mvvm.ComponentModel;
namespace ViewModels;
public partial class Position : ObservableObject
{
[ObservableProperty]
private double _x;
[ObservableProperty]
private double _y;
public Position(double x = 0.0, double y = 0.0)
{
X = x;
Y = y;
}
}
Now you can declare a MainViewModel containing a ObservableCollection<Position> property that will be used as an itemsource. For demonstration purposes, the values for the positions will be changed continuously based upon a DispatchTimer:
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.ObjectModel;
using System.Windows.Threading;
namespace PresentationLayer.ViewModels;
public class MainViewModel : ObservableObject
{
private DispatcherTimer _timer = new();
private Random _random = new();
// binding properties
public ObservableCollection<Position> TextPositions { get; private set; } = new();
public MainViewModel()
{
TextPositions.Add(new(10, 10));
TextPositions.Add(new(20, 100));
TextPositions.Add(new(50, 300));
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += OntimerTick;
_timer.Start();
}
private void OntimerTick(object? sender, EventArgs e)
{
foreach (var item in TextPositions)
{
item.X = (item.X + _random.Next(30)) % 600;
item.Y = (item.Y + _random.Next(20)) % 350;
}
}
}
Finally, this MainViewModel can be used as datacontext for a WPF-Window displaying the positions in an ItemsControl using a datatemplate to construct the Textvalues to be displayed.
The positioning is handled using an ItemsControl.ItemContainerStyle attribute binding the position of the items.
Note: for this to work, you need to specify the ItemsPanel should be of type Canvas since the binding for the Location uses Canvas.Top and Canvas.Left.
The MainWindow (using a MainViewModelinstance as datacontext):
<Canvas>
<ItemsControl ItemsSource="{Binding TextPositions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate >
<DataTemplate>
<Border BorderBrush="Blue" BorderThickness="1" CornerRadius="2" Background="LightSkyBlue">
<WrapPanel>
<TextBlock Text="{Binding X}" />
<TextBlock Text=", "/>
<TextBlock Text="{Binding Y}" />
</WrapPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
For the positioning of controls or display-elements, you can use binding to a property in your ViewModel.
In the example below I am using a Canvas and bind the Canvas.Top attribute of a line to the LinePos property in the Viewmodel.
An alternative could be to bind the Rendertransform instead...
<Canvas>
<Line X1="0" Y1="10" X2="500" Y2="10" Stroke="Blue" StrokeThickness="3" Canvas.Top="{Binding LinePos}" />
</Canvas>
And the ViewModel:
public class MainViewModel : ObservableObject
{
private int _linePos;
private DispatcherTimer _timer = new();
// binding properties
public int LinePos
{
get => _linePos;
set => SetProperty(ref _linePos, value);
}
public MainViewModel()
{
_timer.Interval = TimeSpan.FromMilliseconds(10);
_timer.Tick += OnTimerTick;
_timer.Start();
}
private void OnTimerTick(object? sender, EventArgs e)
{
LinePos = (LinePos+1) % 500;
}
}

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.

ZoomExtents method call works different than activating ZoomExtents through gesture

I've been working on a small 3D preview window in a MVVM style application... The view is created then its data context is set. Therefore it seems that ZoomExtentsWhenLoaded="True" doesn't seem to help do what I need. I need something like, ZoomExtentsWhenDataContextChanges.
Interestingly, I've found that if I use a mouse gesture like the one defined below, I can physically click on the HelixViewport3D and it will perform a ZoomExtents.
HelixViewport3D.ZoomExtentsGesture = new MouseGesture(MouseAction.LeftDoubleClick);
However, if do something like this...
HelixViewport3D.DataContextChanged += (o, e) => ResetCamera();
private void ResetCamera()
{
var dc = HelixViewport3D.DataContext as WellSurveyPlot3DViewModel;
HelixViewport3D.ResetCamera();
HelixViewport3D.Camera = dc.PerspectiveCamera;
HelixViewport3D.ZoomExtents();
}
The viewport does zoom, it just doesn't center itself, like it does when activating ZoomExtents when using the mouse gesture.
I tried ResetCamera, and several other things... What is the standard way of dealing with keeping a viewport around and swapping out the DataContext instead of creating a new one each time?
I fixed this with an attached property. I read through the HelixViewport3D source code and got this idea, after noticing how the camera works. It seems an update to the default camera through a property binding doesn't really do anything after the control is initialized.
public static class HelixViewport3DZoomExtent
{
private static readonly Type OwnerType = typeof(HelixViewport3DZoomExtent);
public static readonly DependencyProperty ZoomExtentsOnUpdateProperty = DependencyProperty.RegisterAttached("ZoomExtentsOnUpdate", typeof(bool), OwnerType, new PropertyMetadata(false, OnDataContextChanged));
public static bool GetZoomExtentsOnUpdate(DependencyObject obj)
{
return (bool)obj.GetValue(ZoomExtentsOnUpdateProperty);
}
public static void SetZoomExtentsOnUpdate(DependencyObject obj, bool value)
{
obj.SetValue(ZoomExtentsOnUpdateProperty, value);
}
private static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var viewport = d as HelixViewport3D;
if (viewport == null) return;
if (viewport.DataContext == null) return;
viewport.Camera = viewport.DefaultCamera;
viewport.ZoomExtents();
}
}
Here is the Xaml
<Border BorderBrush="Black" BorderThickness="1">
<Grid>
<h:HelixViewport3D Name="HelixViewport3D"
PanGesture="LeftClick"
DataContext="{Binding PreviewPlot, UpdateSourceTrigger=PropertyChanged}"
DefaultCamera="{Binding PerspectiveCamera, UpdateSourceTrigger=PropertyChanged}"
services:HelixViewport3DZoomExtent.ZoomExtentsOnUpdate="{Binding RelativeSource={RelativeSource AncestorType={x:Type views:WellSurveyPlot3DPreview}},
Path=DataContext.PreviewUpdatedReZoom, UpdateSourceTrigger=PropertyChanged}">
<h:SunLight/>
<h:TubeVisual3D Path="{Binding TubePath}" Diameter="75" ThetaDiv="12" IsPathClosed="False" Fill="LightGray"/>
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength}" MajorDistance="{Binding MajorGridLines}" Thickness="25"
MinorDistance="{Binding MajorGridLines, UpdateSourceTrigger=PropertyChanged}" LengthDirection="1,0,0" Normal="0,0,1"
Center="{Binding BottomPlaneCenter,UpdateSourceTrigger=PropertyChanged}" Fill="Red" />
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength, UpdateSourceTrigger=PropertyChanged}" LengthDirection="0,0,1" Normal="1,0,0" Thickness="25"
MajorDistance="{Binding MajorGridLines}" MinorDistance="{Binding MajorGridLines}"
Center="{Binding BackLeftPlaneCenter, UpdateSourceTrigger=PropertyChanged}" Fill="Blue" />
<h:GridLinesVisual3D Width="{Binding GridLength}" Length="{Binding GridLength, UpdateSourceTrigger=PropertyChanged}" LengthDirection="1,0,0" Normal="0,1,0" Thickness="25"
MajorDistance="{Binding MajorGridLines}" MinorDistance="{Binding MajorGridLines}"
Center="{Binding BackRightPlaneCenter,UpdateSourceTrigger=PropertyChanged}" Fill="Green" />
</h:HelixViewport3D>
<Button Content="Open Well Viewer" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{Binding OpenWindowCmd}"/>
</Grid>
</Border>
In my view model I have to toggle my PreviewUpdateReZoom property.
private void LoadSurveyPoints(List<WellSurveyPointCalculated> surveyPoints)
{
_coordinatesCalculator = _calcGlobalCoordsFactory.Create(surveyPoints);
_wellXyzCoordinates = _coordinatesCalculator.PlotGlobalCoordinates(100).ToList();
PreviewPlot = WellSurveyPlot3DViewModel();
PreviewUpdatedReZoom = false;//Toggle true false to send property changed and get attached property to fire.
PreviewUpdatedReZoom = true;
}
This now works such that every new item drawn into the viewport has the correct camera settings and zooms to extents...

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 ;)

showing multiple pushpins on a map in windows phone using mvvm model

I have the following view in my mvvm model based app which should display all the pushpins I bind to it using binding property "PushPinLocation" from my view model.
<MapNS:Map
Center="{Binding MapCenter, Mode=TwoWay}" Margin="0,0,-5,0"
CartographicMode="{Binding MapMode, Mode=TwoWay}"
LandmarksEnabled="True" PedestrianFeaturesEnabled="True"
ZoomLevel="{Binding MapZoomLevel, Mode=TwoWay}"
Foreground="AliceBlue" Grid.Row="1" Height="713" Width="425"
x:Name="mapPanoramaAddress" >
<!--Adding Location to show map initially until the data arrived-->
<maptk:MapExtensions.Children>
<maptk:MapItemsControl Name="StoresMapItemsControl" >
<maptk:MapItemsControl.ItemTemplate>
<DataTemplate>
<maptk:Pushpin x:Name="PushPins" Background="White"
GeoCoordinate="{Binding PushPinLocation}"
Content="{Binding PushPinDisplayText}"
Visibility="Visible" />
</DataTemplate>
</maptk:MapItemsControl.ItemTemplate>
</maptk:MapItemsControl>
<maptk:UserLocationMarker GeoCoordinate="{Binding PushPinLocation}" x:Name="UserLocationMarker" Visibility="Visible" />
</maptk:MapExtensions.Children>
</MapNS:Map>
In the geolocator positionchanged event which triggers for every few meters I am setting the value for binding property "PushPinLocation" (from my view model) which is common for pushpin and location marker.
//PushPinLocation
private GeoCoordinate _PushPinLocation = new GeoCoordinate(40.712923, -74.013292); //cannot assign null
public GeoCoordinate PushPinLocation
{
get { return _PushPinLocation; }
set
{
if (_PushPinLocation != value)
{
_PushPinLocation = value;
RaisePropertyChanged("PushPinLocation");
}
}
}
in the same viewmodel geolocator_Position changed event I am setting the pushpinlocation:
private void geolocator_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
this.PushPinLocation = args.Position.Coordinate.ToGeoCoordinate();
}
But I always see the latest one showing up and old ones are never shown on the map.Is there any way I can retain the old ones as well.
This post is a year old, but unanswered, so here is my answer:
Instead of binding to a single PushPinLocation, use a collection. In your ViewModel, add this:
private List<GeoCoordinate> _pushPinLocations;
public List<GeoCoordinate> PushPinLocations
{
get { return _pushPinLocations; }
set
{
_pushPinLocations = value;
OnPropertyChanged("PushPinLocations");
}
}
and change your event to:
private void geolocator_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
this.PushPinLocations.Add(args.Position.Coordinate.ToGeoCoordinate());
}
That will add the new location to the list and as long as its bound to this list of locations, all pins will show.
<maptk:MapItemsControl Name="StoresMapItemsControl" >
<maptk:MapItemsControl.ItemTemplate>
<DataTemplate>
<maptk:Pushpin x:Name="PushPins" Background="White"
GeoCoordinate="{Binding PushPinLocations}"
Content="{Binding PushPinDisplayText}"
Visibility="Visible" />
</DataTemplate>
</maptk:MapItemsControl.ItemTemplate>
</maptk:MapItemsControl>

Categories