Home > database >  How to Create a User Control that Contains a List of Another User Control
How to Create a User Control that Contains a List of Another User Control

Time:11-06

I recently asked this question (KitchenInfoViewModel

And here is one of the rows, after pressing Edit:

ControllerName Row

I've used code for a similar form, but can't seem to even get anything to display. How would I go about creating this?

Here is what I have so far:

DescriptionInfoControl.xaml and DescriptionInfoControl.xaml.cs

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:desc="clr-namespace:StagingApp.Controls.Library.Custom"
    xmlns:converter="clr-namespace:StagingApp.Controls.Library.Converters"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:cal="http://www.caliburnproject.org">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/StagingApp.Styling;component/Styles/Staging.TextBlocks.xaml" />
        <ResourceDictionary Source="pack://application:,,,/StagingApp.Styling;component/Styles/Staging.TextBoxes.xaml" />
        <ResourceDictionary Source="pack://application:,,,/StagingApp.Styling;component/Styles/Staging.Buttons.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <converter:BoolToVisConverter x:Key="BoolToVisConverter" />
    <Style TargetType="{x:Type desc:DescriptionInfoControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type desc:DescriptionInfoControl}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="DescriptionsColumn" />
                            <ColumnDefinition />
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock
                            Grid.Column="0"
                            Text="{Binding DescriptionSource.Description, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                            Style="{StaticResource DeviceInfoPropertyTextStyle}" />
                        <TextBox
                            x:Name="PART_TextBox"
                            Grid.Column="1"
                            HorizontalContentAlignment="Center"
                            Style="{StaticResource DeviceInfoTextBoxStyle}" />
                        <Grid
                            Grid.Column="2">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>
                            <StackPanel
                                Style="{StaticResource EditButtonStackPanelStyle}"
                                Grid.Column="0">
                                <Button
                                    Visibility="{Binding DescriptionSource.BindingName, Converter={StaticResource BoolToVisConverter}, FallbackValue=Visible}"
                                    Style="{StaticResource DeviceInfoEditButtonStyle}">
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="Click">
                                            <cal:ActionMessage MethodName="Edit">
                                                <cal:Parameter Value="{Binding DescriptionSource.Property.Name}" />
                                            </cal:ActionMessage>
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                    Edit
                                </Button>
                            </StackPanel>
                            <StackPanel
                                Style="{StaticResource EditButtonStackPanelStyle}"
                                Grid.Column="0"
                                Visibility="{Binding DescriptionSource.BindingName, Converter={StaticResource BoolToVisConverter}, FallbackValue=Visible}">
                                <Button
                                    Style="{StaticResource DeviceInfoEditOkButtonStyle}">
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="Click">
                                            <cal:ActionMessage MethodName="OkEdit">
                                                <cal:Parameter Value="{Binding DescriptionSource.Property.Name}" />
                                            </cal:ActionMessage>
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                    OK
                                </Button>
                                <Button
                                    Style="{StaticResource DeviceInfoEditCancelButtonStyle}">
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="Click">
                                            <cal:ActionMessage MethodName="CancelEdit">
                                                <cal:Parameter Value="{Binding DescriptionSource.Property.Name}" />
                                            </cal:ActionMessage>
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                    CANCEL
                                </Button>
                            </StackPanel>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

namespace StagingApp.Controls.Library.Custom;

[TemplatePart(Name = _textBoxTemplateName, Type = typeof(TextBox))]
public class DescriptionInfoControl : Control
{
    private const string _textBoxTemplateName = "PART_TextBox";
    private TextBox? _partTextBox;
    private Binding? _textBinding;

    public override void OnApplyTemplate()
    {
        _partTextBox = GetTemplateChild(_textBoxTemplateName) as TextBox;
        SetBindingPartTextbox();
    }

    private void SetBindingPartTextbox()
    {
        if (_partTextBox is TextBox tbox)
        {
            if (_textBinding is null)
            {
                BindingOperations.ClearBinding(tbox, TextBox.TextProperty);
            }
            else
            {
                tbox.SetBinding(TextBox.TextProperty, _textBinding);
            }
        }
    }

    static DescriptionInfoControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(DescriptionInfoControl),
            new FrameworkPropertyMetadata(typeof(DescriptionInfoControl)));
    }

    public DescriptionDto DescriptionSource
    {
        get => (DescriptionDto)GetValue(DescriptionSourceProperty);
        set => SetValue(DescriptionSourceProperty, value);
    }

    // Using a DependencyProperty as the backing store for Description.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DescriptionSourceProperty =
        DependencyProperty.Register(
        nameof(DescriptionSource),
        typeof(DescriptionDto),
        typeof(DescriptionInfoControl),
        new PropertyMetadata(null, DescriptionSourceChangedCallback));

    private static readonly PropertyPath _newValuePropertyPath =
        new(typeof(DescriptionDto).GetProperty(nameof(DescriptionDto.NewValue)));

    private static void DescriptionSourceChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DescriptionInfoControl control = (DescriptionInfoControl)d;
        Binding? binding = null;
        if (e.NewValue is DescriptionDto description)
        {
            binding = new Binding
            {
                Path = _newValuePropertyPath,
                Source = description.Source,
                Mode = BindingMode.TwoWay
            };
        }
        control._textBinding = binding;
        control.SetBindingPartTextbox();
    }
}

DescriptionsInfoListControl.xaml and DescriptionsInfoListControl.xaml.cs

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:desc="clr-namespace:StagingApp.Controls.Library.Custom"
                    xmlns:models="clr-namespace:StagingApp.Controls.Library.Models">

    <Style TargetType="{x:Type desc:DescriptionsInfoListControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type desc:DescriptionsInfoListControl}">
                    <ItemsControl
                        ItemsSource="{TemplateBinding Descriptions}"
                        HorizontalContentAlignment="Stretch"
                        Grid.IsSharedSizeScope="True">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate
                                DataType="{x:Type models:DescriptionDto}">
                                <desc:DescriptionInfoControl
                                   DescriptionSource="{Binding}" />
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
</ResourceDictionary>

namespace StagingApp.Controls.Library.Custom;
public class DescriptionsInfoListControl : Control
{
    static DescriptionsInfoListControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(DescriptionsInfoListControl),
            new FrameworkPropertyMetadata(typeof(DescriptionsInfoListControl)));
    }

    public ReadOnlyCollection<DescriptionDto> Descriptions
    {
        get => (ReadOnlyCollection<DescriptionDto>)GetValue(DescriptionsProperty);
        set { SetValue(_descriptionsPropertyKey, value); }
    }

    private static readonly ReadOnlyCollection<DescriptionDto> _descriptionsEmpty = Array.AsReadOnly(Array.Empty<DescriptionDto>());

    private static readonly DependencyPropertyKey _descriptionsPropertyKey =
        DependencyProperty.RegisterReadOnly(
            nameof(Descriptions),
            typeof(ReadOnlyCollection<DescriptionDto>),
            typeof(DescriptionsInfoListControl),
            new PropertyMetadata(_descriptionsEmpty));

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DescriptionsProperty = _descriptionsPropertyKey.DependencyProperty;

    public DescriptionsInfoListControl()
    {
        DataContextChanged  = OnDataContextChanged;
    }

    private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        ReadOnlyCollection<DescriptionDto> descriptions;
        if (e.NewValue is null)
        {
            descriptions = _descriptionsEmpty;
        }
        else
        {
            descriptions = DescriptionPropertyList.GetDescriptions(e.NewValue);
        }

        Descriptions = descriptions;
    }
}

For the full code, please look at https://github.com/hmsiegel/StagingApp

Any help in getting these InfoControls and InfoViews to work and display would be great.

Thanks

CodePudding user response:

I added methods to remember the entered value and reset the entered value to DescriptionDto.

namespace StagingApp.Controls.Library.Models;
public class DescriptionDto : INotifyPropertyChanged
{

    public DescriptionDto(string? description,
                              PropertyInfo property,
                              object? source)
    {
        if (property.GetGetMethod() is not MethodInfo method)
        {
            throw new ArgumentException("Property must have a public getter.", nameof(property));
        }
        if (!method.IsStatic)
        {
            if (source is null)
                throw new ArgumentException("For Source=null, Property must be static.", nameof(property));

            if (property != source.GetType().GetProperty(property.Name))
                throw new ArgumentException("This property is not from this Source.", nameof(property));
        }

        Description = description ?? string.Empty;
        Property = property;
        Source = source;
        RefreshNewValue();
    }

    public string? Description { get; }
    public PropertyInfo Property { get; }
    public object? Source { get; }

    private string? _newValue;
    public string? NewValue
    {
        get => _newValue;
        set
        {
            _newValue = value;
            PropertyChanged?.Invoke(this, NewValueEventArgs);
        }
    }
    private static readonly PropertyChangedEventArgs NewValueEventArgs = new PropertyChangedEventArgs(nameof(NewValue));

    public event PropertyChangedEventHandler? PropertyChanged;

    public DescriptionDto SetSource(object? newSource) =>
        new(Description!, Property, newSource);

    public override string ToString() =>
        $"{(string.IsNullOrWhiteSpace(Description) ? string.Empty : $"[{Description}] ")}({Source?.GetType().Name}).{Property.Name}: {NewValue}";


    public void RefreshNewValue()
    {
        NewValue = Property.GetValue(Source)?.ToString();
    }

    public void UpdateProperty()
    {
        Property.SetValue(Source, NewValue);
    }

}

I added commands for the row buttons to the DescriptionControl.

using System.Windows.Input;

namespace StagingApp.Controls.Library.Custom;

public partial class DescriptionControl : Control
{
    /// <summary>Sets the DescriptionControl to edit mode: <see cref="DescriptionControl.IsReadOnly"/> = <see langword="false"/>.</summary>
    public static RoutedUICommand Edit { get; } = new RoutedUICommand("Go to edit mode.", nameof(Edit), typeof(DescriptionControl));

    /// <summary>Updates the value of the source property <see cref="DescriptionDto.Property"/> with the value received
    /// from the input field <see cref="DescriptionDto.NewValue"/>.</summary>
    public static RoutedUICommand OK { get; } = new RoutedUICommand("Accept changes.", nameof(OK), typeof(DescriptionControl));

    /// <summary>Returns the value of the input field <see cref="DescriptionDto.NewValue"/>
    /// to the value of the source property <see cref="DescriptionDto.Property"/>
    /// and cancels the edit mode: <see cref="DescriptionControl.IsReadOnly"/> = <see langword="true"/>.</summary>
    public static RoutedUICommand Cancel { get; } = new RoutedUICommand("Undo changes and edit mode.", nameof(Cancel), typeof(DescriptionControl));
}
using System.Windows.Input;

namespace StagingApp.Controls.Library.Custom;

public partial class DescriptionControl : Control
{
    public DescriptionControl()
    {
        ProtectedIsReadOnly = IsReadOnly;

        // Initializing a Routed Commands Binding.
        CommandBinding editCommand = new CommandBinding() { Command = Edit };
        editCommand.CanExecute  = OnCanExecute;
        editCommand.Executed  = OnExecuted;

        CommandBinding cancelCommand = new CommandBinding() { Command = Cancel };
        cancelCommand.CanExecute  = OnCanExecute;
        cancelCommand.Executed  = OnExecuted;

        CommandBinding OkCommand = new CommandBinding() { Command = OK };
        OkCommand.CanExecute  = OnCanExecute;
        OkCommand.Executed  = OnExecuted;

        // Save a Routed Commands Binding.
        CommandBindings.Add(editCommand);
        CommandBindings.Add(cancelCommand);
        CommandBindings.Add(OkCommand);
    }

    private void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (e.Command == Edit)
        {
            e.CanExecute = ProtectedIsReadOnly;
        }
        else if (e.Command == Cancel || e.Command == OK)
        {
            e.CanExecute = !ProtectedIsReadOnly;
        }
    }

    private void OnExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        if (e.Command == Edit)
        {
            IsReadOnly = false;
        }
        else if (e.Command == Cancel || e.Command == OK)
        {
            if (e.Command == Cancel)
            {
                IsReadOnly = true;
                ProtectedDescriptionSource?.RefreshNewValue();

                if (_partTextBox is not null)
                {
                    BindingOperations.GetBindingExpressionBase(_partTextBox, TextBox.TextProperty)
                        ?.UpdateTarget();
                }
            }
            else
            {
                ProtectedDescriptionSource?.UpdateProperty();
            }
        }
    }
}

I added buttons for the row to the DescriptionControl Template. I did not style them - do it yourself as you like.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:desc="clr-namespace:StagingApp.Controls.Library.Custom">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/StagingApp.Styling;component/Styles/Staging.TextBlocks.xaml" />
        <ResourceDictionary Source="pack://application:,,,/StagingApp.Styling;component/Styles/Staging.TextBoxes.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <BooleanToVisibilityConverter x:Key="booleanToVisibility"/>
    <Style TargetType="{x:Type desc:DescriptionControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type desc:DescriptionControl}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="DescriptionsColumn"/>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding DescriptionSource.Description, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
                                   Style="{StaticResource ConfigureTextBlockStyle}"/>
                        <TextBox x:Name="PART_TextBox" Grid.Column="1"
                                 Style="{StaticResource ConfigureTextBox}"
                                 IsReadOnly="{TemplateBinding IsReadOnly}"/>
                        <Button Grid.Column="2" Padding="15 5" 
                                Command="{x:Static desc:DescriptionControl.Edit}"
                                Content="{Binding Command.Name, RelativeSource={RelativeSource Self}}"
                                ToolTip="{Binding Command.Text, RelativeSource={RelativeSource Self}}"
                                Visibility="{Binding IsEnabled, RelativeSource={RelativeSource Self}, Converter={StaticResource booleanToVisibility}}"/>
                        <Button Grid.Column="2" Padding="15 5" 
                                Command="{x:Static desc:DescriptionControl.OK}"
                                Content="{Binding Command.Name, RelativeSource={RelativeSource Self}}"
                                ToolTip="{Binding Command.Text, RelativeSource={RelativeSource Self}}"
                                Visibility="{Binding IsEnabled, RelativeSource={RelativeSource Self}, Converter={StaticResource booleanToVisibility}}"/>
                        <Button Grid.Column="3" Padding="15 5" 
                                Command="{x:Static desc:DescriptionControl.Cancel}"
                                Content="{Binding Command.Name, RelativeSource={RelativeSource Self}}"
                                ToolTip="{Binding Command.Text, RelativeSource={RelativeSource Self}}"
                                Visibility="{Binding IsEnabled, RelativeSource={RelativeSource Self}, Converter={StaticResource booleanToVisibility}}"/>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Fixed a few more bugs. I will not describe them here. Pay attention to getting the filename correctly (Bootstrapper.cs):

    public const string SettingsFileName = "appsettings.json";
    public static readonly string SettingsFileFullName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty, SettingsFileName);
    private static IConfiguration AddConfiguration()
    {
        IConfigurationBuilder builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile(SettingsFileFullName, false, false);

        return builder.Build();
    }

I committed all the changes to the eldhasp branch.

  • Related