Search code examples
c#xamldata-bindingwin-universal-apptwo-way

two-way binding works for one item, but not other


I am currently building an uwp where users can add custom xaml-elements to the GUI and set certain properties. There are also general properties of the page that can be set. To do this there is a properties panel on the designpage.In this properties panel I want to either display the changeable properties from the xaml-element that is selected or (if the user selects one specific element) it should display the general properties. I want to use two-way databinding so that the values are dynamically displayed in the property panel, but also so that changes made in the propertypanel are passed to the source.

I had no problems implementing this two-way binding for displaying the details of the selected xaml-element, but for the general properties the binding appears to go only one-way. When a general property is changed in code, it is passed to the target object, but when data is changed in the GUI it doesn't pass the data to the source.

So I have searched the web and tried modifying my code by adding dependency properties, changing private<-->public, implementing INotifyPropertyChanged in various ways, etc. Unfortunately none of it seems to be working.

Below I will provide the both the working and non-functional c# code/xaml. If anybody can spot the problem I'd be most grateful.

General xaml for the target control in design page

<StackPanel x:Name="PropsPanel" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" Orientation="Vertical" Background="GhostWhite" BorderThickness="1" BorderBrush="GhostWhite" Margin="4">
         <TextBlock Text="Properties" Margin="4,4,0,40" Foreground="Black" FontWeight="Bold"/>
         <ContentControl Name="PropertiesPanel" Foreground="Black">
         </ContentControl>
</StackPanel>

Datatemplate xaml with working two-way binding for properties xaml-element

<DataTemplate x:Key="TimerElementTemplate">
    <Grid HorizontalAlignment="Stretch">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Margin="2" Text="Timer" FontWeight="SemiBold"/>
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="2" Text="Column "/>
        <TextBlock Grid.Row="1" Grid.Column="1" Margin="2" Text="{Binding Path=ShowColumn, Mode=OneWay}" />
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="2" Text="Row " />
        <TextBlock Grid.Row="2" Grid.Column="1" Margin="2" Text="{Binding Path=ShowRow, Mode=OneWay}"/>
        <TextBlock Grid.Row="3" Grid.Column="0" Margin="2" Text="Name "/>
        <TextBox Grid.Row="3" Grid.Column="1" BorderThickness="1" Margin="2" Text="{Binding Path=ElementName, Mode=TwoWay}" />
        <TextBlock Grid.Row="4" Grid.Column="0" Margin="2" Text="Label " />
        <TextBox Grid.Row="4" Grid.Column="1" Margin="2" BorderThickness="1" Text="{Binding Path=Label, Mode=TwoWay}" />
    </Grid>
</DataTemplate>

Working code for setting the datacontext

private void GOnPointerReleased(object sender, PointerRoutedEventArgs pointerRoutedEventArgs)
    {
        if (_selectedGuiElement != null)
        {
            _selectedGuiElement.BorderThickness = new Thickness(0);
        }
        PropertiesPanel.DataContext = sender;
        PropertiesPanel.ContentTemplate = Resources[sender.GetType().Name + "Template"] as DataTemplate;
        var element = sender as GuiElement;
        element.BorderBrush = new SolidColorBrush(Colors.Crimson);
        element.BorderThickness = new Thickness(2.0);
        _selectedGuiElement = element;
        
    }

The elements with the displayable properties are GUIelements that inherit from the Grid Xaml-control. The properties I display are marked with the GUIElementProperty-attribute. The code for the GUI-element is below:

public class GuiElement : Grid
{
    protected string _elementType;

    public GuiElement(bool design)
    {
        DesignState = design;
        SetElementType();
        AddContent();
        if(DesignState)
        AllowDrop = true;
    }

    //overridable method to set Type and Type related properties
    public virtual void SetElementType()
    {
        _elementType = "";
        GuiBackground = new SolidColorBrush(Colors.DarkGray);
    }
    //overridable method to set lay-out content for each type
    public virtual void AddContent()
    {
        Background = GuiBackground;
        BorderThickness = new Thickness(1);
        BorderBrush = new SolidColorBrush(Colors.GhostWhite);
        SetColumnSpan(this, ColumnSpan);
        SetRowSpan(this, RowSpan);
    }


    // shortcuts for Grid.Column and Grid.Row properties and other general properties
    [GuiElementProperty("lbl", "Column", 2)]
    public int Column
    {
        get { return GetColumn(this); }
        set { SetColumn(this, value); }
    }

    public string ShowColumn
    {
        get { return Column.ToString(); }
    }

    [GuiElementProperty("lbl", "Row", 1)]
    public int Row
    {
        get { return GetRow(this); }
        set { SetRow(this, value); }
    }

    public string ShowRow
    {
        get { return Row.ToString(); }
    }

    public int ColumnSpan { get; set; } = 1;

    public int RowSpan { get; set; } = 1;

    public bool DesignState { get; set; }

    public SolidColorBrush GuiBackground { get; set; }

    public int Id { get; set; }

    [GuiElementProperty("lbl", "Type", 0)]
    public string ElementType { get { return _elementType; } }
}

}

So far the working code...now the problematic part

Datatemplate xaml for displaying the general properties with dysfunctional two-way binding

<DataTemplate x:Key="StartElementTemplate">
    <Grid HorizontalAlignment="Stretch">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Margin="2" Text="Session" FontWeight="SemiBold"/>
        <!--<Button Foreground="GhostWhite" Grid.Row="0" Grid.Column="1" Content="X" Click="ButtonDeleteFromProperties_OnClick"></Button>-->
        <TextBlock Grid.Row="1" Grid.Column="0" Margin="2" Text="Name "/>
        <TextBlock Grid.Row="1" Grid.Column="1" Margin="2" Text="{Binding Path=DesignName, Mode=OneWay}"/>
        <TextBlock Grid.Row="2" Grid.Column="0" Margin="2" Text="Time Limit (true/false) "/>
        <TextBox Grid.Row="2" Grid.Column="1" Margin="2" BorderThickness="1" Text="{Binding Path=LimitedTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <TextBlock Grid.Row="3" Grid.Column="0" Margin="2" Text="Max. duration (hh:mm:ss) "/>
        <TextBox Grid.Row="3" Grid.Column="1" BorderThickness="1" Margin="2" Text="{Binding Path=Timelimit, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</DataTemplate>

the code for the class with the general properties

 public class SessionProps : INotifyPropertyChanged   
{   private string _designname;
   private bool _limitedtime;
   private TimeSpan _timelimit;

   [GuiElementProperty("txtbx", "Design name", 0)]
    public string DesignName
    {
        get { return _designname; }
        set
        {
            _designname = value;
            NotifyPropertyChanged();
        }
    }

    [GuiElementProperty("bool", "Time limit", 1)]
   public bool LimitedTime
    {
        get { return _limitedtime; }
        set
        {
            _limitedtime = value;
            NotifyPropertyChanged();
        }
    }

    [GuiElementProperty("txtbx", "Max. Duration (hh:mm:ss)", 2)]
   public TimeSpan Timelimit
    {
       get { return _timelimit; }
       set
       {
           _timelimit = value;
            NotifyPropertyChanged();
        } 
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Creating an instance of the general properties class

 public sealed partial class DesignPage : Page
{
    private SessionProps _props = new SessionProps() {DesignName = "", LimitedTime = false, Timelimit = TimeSpan.Zero};

code binding control to this source

 private void SOnPointerReleased(object sender, PointerRoutedEventArgs e)
    {
        if (_selectedGuiElement != null)
        {
            _selectedGuiElement.BorderThickness = new Thickness(0);
        }
        PropsPanel.DataContext = _props;
        PropertiesPanel.ContentTemplate = Resources[sender.GetType().Name + "Template"] as DataTemplate;
        var element = sender as GuiElement;
        element.BorderBrush = new SolidColorBrush(Colors.Crimson);
        element.BorderThickness = new Thickness(2.0);
        _selectedGuiElement = element;
    }

I'm hoping there is just something small I'm overlooking here, but any help will be appreciated! I hope I have provided sufficient information, but if there is anything that's still unclear, please ask.


Solution

  • The reason is that the LimitedTime is bool type, Timelimit is TimeSpan type and the text in TextBox is string type. When you edit the TextBox, it will get an Error: Converter failed to convert value of type 'Windows.Foundation.String' to type 'Boolean';

    You can create a class that allows you to convert the format of your data between the source and the target by inheriting from IValueConverter.

    You should always implement Convert with a functional implementation, but it's fairly common to implement ConvertBack so that it reports a not-implemented exception. You only need a ConvertBack method in your converter if you are using the converter for two-way bindings, or using XAML for serialization.

    For more info, see INotifyPropertyChanged.

    For example:

    public class BoolFormatter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            return value.ToString();
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            if (value.ToString() == "true" || value.ToString() == "True")
            {
                return true;
            }
            else if (value.ToString() == "false" || value.ToString() == "False")
            {
                return false;
            }
            else
            {
                return "";
            }
        }
    }
    

    Create TimeSpanFormatter class:

    public class TimeSpanFormatter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value != null)
            {
                return value.ToString();
            }
            else
            {
                return null;
            }
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            TimeSpan span;
    
            if (TimeSpan.TryParse(value.ToString(), out span))
            {
                return span;
            }
            else
            {
                return "";
            }
        }
    }
    

    In XAML:

    <Page.Resources>
        <local:BoolFormatter x:Key="BoolConverter" />
        <local:TimeSpanFormatter x:Key="TimeSpanConverter" />
        <DataTemplate>
            <Grid HorizontalAlignment="Stretch">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Grid.Column="0" Margin="2" Text="Session" FontWeight="SemiBold" />
                <!--<Button Foreground="GhostWhite" Grid.Row="0" Grid.Column="1" Content="X" Click="ButtonDeleteFromProperties_OnClick"></Button>-->
                <TextBlock Grid.Row="1" Grid.Column="0" Margin="2" Text="Name " />
                <TextBlock Grid.Row="1" Grid.Column="1" Margin="2" Text="{Binding Path=DesignName, Mode=TwoWay}" />
                <TextBlock Grid.Row="2" Grid.Column="0" Margin="2" Text="Time Limit (true/false) " />
                <TextBox Grid.Row="2" Grid.Column="1" Margin="2" BorderThickness="1" Text="{Binding Path=LimitedTime, Mode=TwoWay,Converter={StaticResource BoolConverter}}" />
                <TextBlock Grid.Row="3" Grid.Column="0" Margin="2" Text="Max. duration (hh:mm:ss) " />
                <TextBox Grid.Row="3" Grid.Column="1" BorderThickness="1" Margin="2" Text="{Binding Path=Timelimit, Mode=TwoWay,Converter={StaticResource TimeSpanConverter}}" />
            </Grid>
        </DataTemplate>
    </Page.Resources>