Binding WindowStartupLocation - c#

I'm trying to make my main window to remember and restore the position and size on startup. So I tried to bind my window's startup location to a properties in my viewmodel as following:
<Window x:Class="MyApp.Views.MainWindow"
...
Width="{Binding Width}"
Height="{Binding Height}"
WindowStartupLocation="{Binding WindowStartupLocation}"
WindowState="{Binding WindowState}"
MinHeight="600"
MinWidth="800"
Closing="OnWindowClosing"
Closed="OnWindowClosed"
ContentRendered="OnMainWindowReady"
...>
My viewmodel:
...
// Default settings
WindowState = (WindowState)FormWindowState.Normal;
this.WindowStartupLocation = WindowStartupLocation.CenterScreen;
Width = 800;
Height = 600;
// check if the saved bounds are nonzero and is visible on any screen
if (Properties.Settings.Default.WindowStartupLocation != Rectangle.Empty &&
IsVisibleOnAnyScreen(Properties.Settings.Default.WindowStartupLocation))
{
this.WindowStartupLocation = WindowStartupLocation.Manual;
this.WindowState = (WindowState)Properties.Settings.Default.WindowState;
Height = Properties.Settings.Default.WindowStartupLocation.Size.Height;
Width = Properties.Settings.Default.WindowStartupLocation.Size.Width;
Left = Properties.Settings.Default.WindowStartupLocation.Left;
Top = Properties.Settings.Default.WindowStartupLocation.Top;
}
...
When i run the application i get a System.Windows.Markup.XamlParseException and Additional information: A 'Binding' cannot be set on the 'WindowStartupLocation' property of type 'MainWindow'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
How should i correct this?

Try using attached behavior which lets you bind the WindowStartupLocation property:
namespace YourProject.PropertiesExtension
{
public static class WindowExt
{
public static readonly DependencyProperty WindowStartupLocationProperty;
public static void SetWindowStartupLocation(DependencyObject DepObject, WindowStartupLocation value)
{
DepObject.SetValue(WindowStartupLocationProperty, value);
}
public static WindowStartupLocation GetWindowStartupLocation(DependencyObject DepObject)
{
return (WindowStartupLocation)DepObject.GetValue(WindowStartupLocationProperty);
}
static WindowExt()
{
WindowStartupLocationProperty = DependencyProperty.RegisterAttached("WindowStartupLocation",
typeof(WindowStartupLocation),
typeof(WindowExt),
new UIPropertyMetadata(WindowStartupLocation.Manual, OnWindowStartupLocationChanged));
}
private static void OnWindowStartupLocationChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
Window window = sender as Window;
if (window != null)
{
window.WindowStartupLocation = GetWindowStartupLocation(window);
}
}
}
}
The usage:
<Window
PropertiesExtension:WindowExt.WindowStartupLocation="{Binding ..}" />
As the error stated, WindowStartupLocation is not a dependency propety, which means you can't bind it. The solution can be either deriving from Window, and creating dependency property, or using attached behavior.

You cannot bind the WindowsStartupLocation, this line generates the error:
WindowStartupLocation="{Binding WindowStartupLocation}"
You can set it to some particular value, like that:
WindowStartupLocation="CenterScreen"

Related

Making slide animation in wpf

I have a grid, which used as container. Grid consist of UserControl, each one has 600px height and 800px width. I want to make slide animation like presentation by switching visible controls.
Here is my xaml code of mainWindow:
<Window x:Class="MessengerWindowsClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MessengerWindowsClient"
xmlns:pages="clr-namespace:MessengerWindowsClient.Pages"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" Closed="Window_Closed">
<Window.Background>
<ImageBrush ImageSource="Resources/background.jpg"></ImageBrush>
</Window.Background>
<Grid x:Name="Container" RenderTransformOrigin="0.5,0.5" SizeChanged="Container_SizeChanged">
<pages:WelcomePage x:Name ="WelcomePage" Visibility="Visible" RegisterPage="{Binding ElementName=RegisterPage}" LoginPage="{Binding ElementName=LoginPage}"/>
<pages:MessagesPage Visibility="Collapsed"/>
<pages:LoginPage x:Name="LoginPage" Visibility="Collapsed" WelcomePage="{Binding ElementName=WelcomePage}"/>
<pages:RegisterPage x:Name="RegisterPage" Visibility="Collapsed" WelcomePage="{Binding ElementName=WelcomePage}"/>
</Grid>
Here is code behind:
public partial class MainWindow : Window
{
private ServiceManager _serviceManager;
private UIElement _currentPage;
public MainWindow()
{
InitializeComponent();
_currentPage = this.Container.Children[0];
this.RegisterPage.RegisterReady += RegisterUser;
this.RegisterPage.ChangePage += ChangePage;
this.WelcomePage.ChangePage += ChangePage;
this.LoginPage.ChangePage += ChangePage;
_serviceManager = new ServiceManager();
}
private void ChangePage(object sender, ChangePageEventArgs e)
{
switch (e.Direction)
{
case ChangePageDirection.Forward:
AnimationManager.AnimateForwardPage(e.NewPage, e.OldPage, Container, this.ActualWidth);
break;
case ChangePageDirection.Backward:
AnimationManager.AnimateBackwardPage(e.NewPage, e.OldPage, Container, this.ActualWidth);
break;
}
}
private async void RegisterUser(object sender, RegisterEventArgs e)
{
var isSucceed = await _serviceManager.RegisterUser(e.Name, e.Username, e.Password.ToString(), e.Email);
e.Password.Dispose();
}
private void Window_Closed(object sender, EventArgs e)
{
_serviceManager.Dispose();
}
private void Container_SizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateLayout();
}
}
}
I tried to use this.ActualWidth, but it gives value that is more than my display resolution. So part of my control goes behind the screen. And after the animation completes it returns back. Using any width property of grid gives wrong value, even with UpdateLayout() on resize event.
Edit:
Screenshots
After animation completed and after _container.HorizontalAlignment = HorizontalAlignment.Stretch;.
Are you trying to animate the width or height of a certain UI element? You need to make a custom animation class that extends AnimationTimeline and define an animation inside a Storyboard in XAML.
You will need to create a custom class GridLengthAnimation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Animation;
namespace Infrastructure.Animations
{
public class GridLengthAnimation : AnimationTimeline
{
protected override Freezable CreateInstanceCore()
{
return new GridLengthAnimation();
}
public override Type TargetPropertyType => typeof(GridLength);
static GridLengthAnimation()
{
FromProperty = DependencyProperty.Register("From", typeof(GridLength),
typeof(GridLengthAnimation));
ToProperty = DependencyProperty.Register("To", typeof(GridLength),
typeof(GridLengthAnimation));
}
public static readonly DependencyProperty FromProperty;
public GridLength From
{
get => (GridLength)GetValue(GridLengthAnimation.FromProperty);
set => SetValue(GridLengthAnimation.FromProperty, value);
}
public static readonly DependencyProperty ToProperty;
public GridLength To
{
get => (GridLength)GetValue(GridLengthAnimation.ToProperty);
set => SetValue(GridLengthAnimation.ToProperty, value);
}
public override object GetCurrentValue(object defaultOriginValue,
object defaultDestinationValue, AnimationClock animationClock)
{
double fromVal = ((GridLength)GetValue(GridLengthAnimation.FromProperty)).Value;
double toVal = ((GridLength)GetValue(GridLengthAnimation.ToProperty)).Value;
if (fromVal > toVal)
{
return new GridLength((1 - animationClock.CurrentProgress.Value) *
(fromVal - toVal) + toVal, GridUnitType.Pixel);
}
else
{
return new GridLength(animationClock.CurrentProgress.Value *
(toVal - fromVal) + fromVal, GridUnitType.Pixel);
}
}
}
}
You can then use this in XAML inside a Storyboard like this:
<Storyboard x:Key="storyboardName">
<support:GridLengthAnimation Storyboard.TargetName="YourElementToAnimate" Storyboard.TargetProperty="Width" From="{Binding StartAnimationWidth}" To="{Binding EndAnimationWidth}" DecelerationRatio="0.9" Duration="0:0:0.6"/>
</Storyboard>
The StartAnimationWidth and EndAnimationWidth are properties of type GridLength and are defined in ViewModel
private GridLength _endAnimationWidth = new GridLength(100);
public GridLength EndAnimationWidth
{
get => _endAnimationWidth;
set => SetProperty(ref _endAnimationWidth,value);
}
You can then trigger the animation from the code behind:
Storyboard sb = Resources["storyboardName"] as Storyboard;
sb.Begin();

Wrong startup position of window, when SizeToContent is Manual, and Window.Top and Window.Left are databound

I've ran into a rather strange issue. It seems that as long as a window's SizeToContent is set to Manual and the dimensions and top/left properties are databound, the Presentation Framework won't update the location of the window when it is shown.
I have a Dialog Window-control which I use to display other UserControls within, and the behavior should be controlled by a ViewModel. Some dialogs I wish to display with SizeToContent.WidthAndHeight and ResizeMode.NoResize, and center them on the Owner-window. Other controls I wish to show in a dialog that can be resized, but opened with a specific size.
I made a test-project just to isolate the issue. Here is my Dialog:
<Window x:Class="DialogTests.Dialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DialogTests"
mc:Ignorable="d"
ResizeMode="CanResize"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
Width="{Binding Path=FormWidth, Mode=TwoWay}"
Height="{Binding Path=FormHeight, Mode=TwoWay}"
Top="{Binding Path=FormTop, Mode=TwoWay}"
Left="{Binding Path=FormLeft, Mode=TwoWay}"
Title="Dialog">
<StackPanel>
<Button Height="100">Button 1</Button>
<Button Width="100">Button 2</Button>
</StackPanel>
</Window>
public partial class Dialog : Window
{
public Dialog()
{
InitializeComponent();
this.DataContext = new DialogVm();
}
}
public class DialogVm : INotifyPropertyChanged
{
private double _formWidth;
private double _formHeight;
private double _formTop;
private double _formLeft;
public DialogVm()
{
FormWidth = 400;
FormHeight = 400;
}
public double FormWidth
{
get { return _formWidth; }
set
{
if (value.Equals(_formWidth)) return;
_formWidth = value;
OnPropertyChanged();
}
}
public double FormHeight
{
get { return _formHeight; }
set
{
if (value.Equals(_formHeight)) return;
_formHeight = value;
OnPropertyChanged();
}
}
public double FormTop
{
get { return _formTop; }
set
{
if (value.Equals(_formTop)) return;
_formTop = value;
OnPropertyChanged();
}
}
public double FormLeft
{
get { return _formLeft; }
set
{
if (value.Equals(_formLeft)) return;
_formLeft = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The reason for having the Dialog databind to these properties is to save the state (dimensions and position), so that it can be used in cases where certain dialogs should be opened using these previous values.
In my main application I just open the dialog by pressing a button:
private void Button_Click(object sender, RoutedEventArgs e)
{
var dialog = new Dialog();
dialog.Owner = this;
dialog.ShowDialog();
}
If you run this, then everything works as expected. The dialog gets sized to fit the contents, and is placed in the center relative to the main application. The setters of FormWidth, FormHeight, FormTop and FormLeft get called. For the dimensions, it's first called from the constructor, then by the PresentationFramework.
However, if you go back to the Dialog xaml, and change SizeToContent="WidthAndHeight" to SizeToContent="Manual", the Dialog will be shown at the top left (0,0) of the screen. The dialog's dimensions are set in the view model contructor (400 x 400) and doesn't get called again (as expected). The issue is, the Setter methods of FormTop and FormLeft are never called. The expected behaviour is that the 400x400 window should be shown in the center of the Owner window.
If I change the Dialog.xaml to remove the databindings, and explicitly set the dimensions, it works as it should though:
<Window x:Class="DialogTests.Dialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DialogTests"
mc:Ignorable="d"
ResizeMode="CanResize"
SizeToContent="Manual"
WindowStartupLocation="CenterOwner"
Width="400"
Height="400"
Title="Dialog">
...
</Dialog>
Is this behavior a bug? Or is there something I am missing? Seems so strange that it works fine as long as SizeToContent is set to WidthAndHeight, or if I remove the bindings entirely and set the attributes in the xaml.
It depends on when the bindings are resolved and when the position of the window is set.
You could set the DataContext of the Dialog window before you call the InitializeComponent() method:
public Dialog()
{
this.DataContext = new DialogVm();
InitializeComponent();
}
Or set the WindowStartupLocation to Manual and calculate the values of your Left and Top source properties:
How do you center your main window in WPF?

Show new Window with DataContext

I want to simply display my UserControl in a separate window, for example by calling
var windowHandler = new WindowHandler();
windowHandler.Show(new SchoolViewModel);
How do I archive this? I have tried the following:
Set the DataTemplate in App.xaml:
<Application.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type viewModel:SchoolViewModel}">
<view:SchoolUserControl />
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
In code-behind call it:
private void Application_Startup(object sender, StartupEventArgs e)
{
var windowHandler = new WindowHandler();
windowHandler.ShowWindow(new SchoolViewModel(), 200, 200);
}
WindowHandler class:
public class WindowHandler
{
public void ShowWindow(object dataContext, int height, int width)
{
Window window = new Window()
{
DataContext = dataContext,
Width = width,
Height = height
};
window.Show();
}
}
It does show a window, but it's empty. Why is it empty? I also set the DataContext in the UserControl's code-behind:
public SchoolUserControl()
{
InitializeComponent();
DataContext = this;
}
Window is by default templated to show Window.Content and not Window.DataContext. So you should assign whatever you want to show as content:
public class WindowHandler
{
public void ShowWindow(object dataContext, int height, int width)
{
Window window = new Window()
{
Content = dataContext,
Width = width,
Height = height
};
window.Show();
}
}
Also, as others noted, you should remove this line:
DataContext = this;
from your SchoolUserControl, because otherwise you won't have access to the templated view-model from within the control. And since SchoolUserControl is part of a DataTemplate, the templated view-model will be automatically available from SchoolUserControl.DataContext.

What's the issue with the way I'm binding to a dependency property?

Note: You can find the project below on github now. https://github.com/ReasonSharp/MyTestRepo
I'm creating a simple list control with a scrollbar that will display a collection of objects I pass to it. When a user clicks on one item, I want it to become a selected item, and when he clicks it again, I want it to be unselected. I store the selected item in a SelectedLocation property. While debugging, the property is set appropriately. However, if I place this list control (LocationListView) onto a window and bind to SelectedLocation (like SelectedLocation="{Binding MyLocation}") in a control, the binding won't work, and if I try to use this MyLocation in another binding in the same window (i.e. <TextBox Text="{Binding MyLocation.ID}"/>, where ID is a dependency property), that binding won't show anything changing as I select different items in the list.
Minimal example is a bit large, please bear with me:
List control
XAML
<UserControl x:Class="MyListView.LocationListView"
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:MyListView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="locationListView">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="myStackPanel"/>
</ScrollViewer>
</Grid>
</UserControl>
Code behind
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace MyListView {
public partial class LocationListView : UserControl {
#region Dependency Properties
public IEnumerable Locations {
get { return (IEnumerable)GetValue(LocationsProperty); }
set { SetValue(LocationsProperty, value); }
}
public static readonly DependencyProperty LocationsProperty =
DependencyProperty.Register("Locations", typeof(IEnumerable), typeof(LocationListView), new PropertyMetadata(null, LocationsChanged));
public MyObject SelectedLocation {
get { return (MyObject)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation", typeof(MyObject), typeof(LocationListView), new PropertyMetadata(null));
#endregion
private static void LocationsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
((LocationListView)o).RegenerateLocations();
if (((LocationListView)o).Locations is ObservableCollection<MyObject>) {
var l = ((LocationListView)o).Locations as ObservableCollection<MyObject>;
l.CollectionChanged += ((LocationListView)o).L_CollectionChanged;
}
}
private void L_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
RegenerateLocations();
}
private Button selectedLV = null;
public LocationListView() {
InitializeComponent();
}
private void RegenerateLocations() {
if (Locations != null) {
myStackPanel.Children.Clear();
foreach (var l in Locations) {
var b = new Button();
b.Content = l;
b.Click += B_Click;
myStackPanel.Children.Add(b);
}
}
selectedLV = null;
}
private void B_Click(object sender, RoutedEventArgs e) {
var lv = (sender as Button)?.Content as MyObject;
if (selectedLV != null) {
lv.IsSelected = false;
if ((selectedLV.Content as MyObject) == SelectedLocation) {
SelectedLocation = null;
selectedLV = null;
}
}
if (lv != null) {
SelectedLocation = lv;
selectedLV = sender as Button;
lv.IsSelected = true;
}
}
}
}
Note the absence of this.DataContext = this; line. If I use it, I get the following binding expression path errors:
System.Windows.Data Error: 40 : BindingExpression path error: 'SillyStuff' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=SillyStuff; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'Locations' (type 'IEnumerable')
System.Windows.Data Error: 40 : BindingExpression path error: 'MySelectedLocation' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=MySelectedLocation; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'SelectedLocation' (type 'MyObject')
Using (this.Content as FrameworkElement).DataContext = this; won't produce these errors, but it won't work either.
Main window
XAML
<Window x:Class="MyListView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyListView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<local:LocationListView Locations="{Binding SillyStuff}" SelectedLocation="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>
<TextBox Text="{Binding MySelectedLocation.ID}" DockPanel.Dock="Top"/>
</DockPanel>
</Grid>
</Window>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class MainWindow : Window {
private MainViewModel vm;
public MainWindow() {
InitializeComponent();
}
[Dependency] // Unity
internal MainViewModel VM {
set {
this.vm = value;
this.DataContext = vm;
}
}
}
}
MainViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MyListView {
class MainViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (PropertyChanged != null)
PropertyChanged(sender, e);
}
private MyObject mySelectedLocation;
public MyObject MySelectedLocation {
get { return mySelectedLocation; }
set {
mySelectedLocation = value;
OnPropertyChanged(this, new PropertyChangedEventArgs("MySelectedLocation"));
}
}
public ObservableCollection<MyObject> SillyStuff {
get; set;
}
public MainViewModel() {
var cvm1 = new MyObject();
cvm1.ID = 12345;
var cvm2 = new MyObject();
cvm2.ID = 54321;
var cvm3 = new MyObject();
cvm3.ID = 15243;
SillyStuff = new ObservableCollection<MyObject>();
SillyStuff.Add(cvm1);
SillyStuff.Add(cvm2);
SillyStuff.Add(cvm3);
}
}
}
MyObject
using System.Windows;
namespace MyListView {
public class MyObject : DependencyObject {
public int ID {
get { return (int)GetValue(IDProperty); }
set { SetValue(IDProperty, value); }
}
public static readonly DependencyProperty IDProperty =
DependencyProperty.Register("ID", typeof(int), typeof(MyObject), new PropertyMetadata(0));
public bool IsSelected {
get; set;
}
public override string ToString() {
return ID.ToString();
}
}
}
App.xaml -- just to save anyone the typing
XAML
<Application x:Class="MyListView.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyListView">
<Application.Resources>
</Application.Resources>
</Application>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class App : Application {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
UnityContainer container = new UnityContainer();
var mainView = container.Resolve<MainWindow>();
container.Dispose();
mainView.Show();
}
}
}
The objective here is to have the value in the TextBox on MainWindow change to the selected item's ID whenever the selected item changes. I could probably do it by creating a SelectedItemChanged event on my LocationListView, and then setting the property manually in a handler, but that seems like a hack. If you place a <ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/> instead of my list control, this works like a charm, so I should be able to make my control work that way too.
Edit: Changed MainViewModel to implement INotifyPropertyChanged as per Pieter's instructions.
Main issues
When you select an item in your custom control, B_Click assigns it to the SelectedLocation property, which calls SetValue internally. However, this overwrites the binding on SelectedLocation - in other words, after that call SelectedLocation is no longer bound to anything. Use SetCurrentValue instead to preserve the binding.
However, bindings won't update their source by default. You'll have to set their Mode to TwoWay. You can do that in XAML: SelectedLocation="{Binding MySelectedLocation, Mode=TwoWay}", or mark the dependency property to use TwoWay binding by default: new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, LocationsChanged).
Finally, make sure that your binding paths are correct. Your text box binds to SelectedLocation, while the property is named MySelectedLocation. These kind of issues are usually logged in the debug output, in this case you should get a message like this:
System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedLocation' property not found on 'object' ''MainViewModel' (HashCode=8757408)'. BindingExpression:Path=SelectedLocation.ID; DataItem='MainViewModel' (HashCode=8757408); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Other issues
I've found a few other issues as well: you're not unregistering L_CollectionChanged when another collection is set, and if the collection is removed, you're not clearing the visible items. The code in B_Click is also troublesome: you're also accessing lv before making sure it's not null, and if the user clicks on an unselected button you're setting SelectedLocation to null before setting it to the newly selected item. Also, when regenerating items, selectedLV (what's 'lv'?) is set to null, but SelectedLocation is left intact...
Also a little tip: your OnPropertyChanged method only needs a single argument: string propertyName. Make it optional and mark it with a [CallerMemberName] attribute, so all that a property setter needs to do is call it without arguments. The compiler will insert the calling property name for you.
Alternatives
Personally, I'd just use a ListView with a custom ItemTemplate:
<ListView ItemsSource="{Binding MyLocations}" SelectedItem="{Binding MySelectedLocation}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Content="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This probably requires a few more modifications to make it look nice, but that's the gist of it. Alternately, you could create an attached behavior that takes care of your desired selection behavior.
Oh boy, that's a lot of code.
Let me begin by highlighting a common mistake, which is setting the control's DataContext to itself. This should be avoided as it tends to screw up absolutely everything.
So. Avoid doing this:
this.DataContext = this;
It is not the responsibility of the UserControl itself to set it's own DataContext, it should be the responsibility of the parent control (such as a Window to set it. Like this:
<Window ...>
<local:MyUserControl DataContext="{Binding SomeProperty}" ... />
If your UserControl was to set its own DataContext, then it will override what the Window sets its DataContext to be. Which will result in the screwing up of absolutely everything.
To bind to a Dependency Property of a UserControl, simply give your control an x:Name and use an ElementName binding, like this:
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDependencyProperty, ElementName=usr}" ... />
What's important to note here is that the DataContext isn't being set at all, so your parent Window is free to set the control's DataContext to whatever it needs to be.
Adding to this, your UserControl can now bind to it's DataContext using a straightforward Path binding.
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDataContextProperty}" ... />
I hope this helps.

Inherited Value in Column

we have a wpf-window with some textboxes and a datagrid.
the textboxes descripe a parent (class a) object and the datagrid lists a collection of "childs" (class b => not derived from class a).
the childs can inherit values from the parent.
for example if the parent (class a) has a property Foo then the child object (class b) has a property Nullable which can either override the value of the parent or inherit the value of the parent.
now the datagrid should display the value in gray (if it is inherited) or in black (if the user overrides the value in the grid cell).
Unfortunatly Binding to InheritedText doesnt work. Does someone have any idea?
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<UserControls:InheritedTextBoxControl
Text="{Binding Path=?}"
InheritedText="{Binding Path=?}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
Thanks in advance
Tobi
--UPDATE--
xaml of InheritedTextBoxControl:
<UserControl x:Class="Com.QueoMedia.CO2Simulationstool.WPF.Utils.UserControls.InheritedTextBoxControl"
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"
mc:Ignorable="d"
Width="Auto"
Height="Auto"
Name="cnt">
<Grid x:Name="LayoutRoot"
Background="White">
<TextBox TextChanged="TextBoxTextChanged"></TextBox>
<TextBlock Name="inheritedText"
IsHitTestVisible="False"
Margin="4,0"
VerticalAlignment="Center"
Opacity="0.5"
FontStyle="Italic"></TextBlock>
</Grid>
CodeBehind:
public partial class InheritedTextBoxControl : UserControl {
private bool _isInherited;
public static readonly DependencyProperty InheritedTextProperty = DependencyProperty.Register("InheritedText", typeof(String), typeof(InheritedTextBoxControl), new PropertyMetadata(""));
public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(InheritedTextBoxControl), new PropertyMetadata(default(string)));
public InheritedTextBoxControl() {
InitializeComponent();
}
public string InheritedText {
get { return (string)GetValue(InheritedTextProperty); }
set {
SetValue(InheritedTextProperty, value);
inheritedText.Text = value;
}
}
private bool IsInherited {
get { return _isInherited; }
set {
_isInherited = value;
if (value) {
inheritedText.Opacity = 0.5;
} else {
inheritedText.Opacity = 0;
}
}
}
public string Text {
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
private void TextBoxTextChanged(object sender, TextChangedEventArgs e) {
if (((TextBox)sender).Text.Length > 0) {
IsInherited = false;
} else {
IsInherited = true;
}
Text = ((TextBox)sender).Text;
}
}
The problem is the setter of your InheritedText property. WPF won't call this setter when the property is set from XAML. See Checklist for Defining a Dependency Property, section Implementing the "Wrapper" for details.
You will have to update inheritedText.Text in a PropertyChangedCallback like this:
public static readonly DependencyProperty InheritedTextProperty =
DependencyProperty.Register(
"InheritedText", typeof(string), typeof(InheritedTextBoxControl),
new PropertyMetadata(string.Empty, InheritedTextChanged));
private static void InheritedTextChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((InheritedTextBoxControl)d).inheritedText.Text = (string)e.NewValue;
}
public string InheritedText
{
get { return (string)GetValue(InheritedTextProperty); }
set { SetValue(InheritedTextProperty, value); } // only call SetValue here
}
If someone is interested in the solution:
we did it using a CellTemplate containing a CustomControl name MaskedTextbox that has three properties (MaskedText, Text, IsMaskTextVisible) and a CellEditingTemplate to override the data.
The values are bound to an InheritableValueViewModel.
Tobi

Categories

Resources