WPF Custom Control Not Binding Correctly

Marc Jeeves 386 Reputation points
2021-06-20T03:00:11.607+00:00

Evening I created a nice UI based on importing an XML file and it all works, i want to replace the button i had with a custom control (see below) i have roughed it out but i cant get the button content to bind.

Can anybody see where i screwed up?

Thanks

Madaxe

New Code

<ItemsControl ItemsSource="{Binding Software}">
                                                            <ItemsControl.ItemTemplate>
                                                                <DataTemplate>
                                                                    <CustomControls:SoftwareInstallStatus_Ctrl
                                                                        ButtonContent="{Binding Name}" 
                                                                        ButtonWidth="100" 
                                                                        HorizontalAlignment="Left" 
                                                                        Margin="5,5,5,5"/>
                                                                </DataTemplate>
                                                            </ItemsControl.ItemTemplate>
                                                        </ItemsControl>

Original Code

<!--<DataTemplate>
    <Button Content="{Binding Name}" 
                                                                Width="100" 
                                                                HorizontalAlignment="Left" 
                                                                Margin="5,5,5,5"
                                                                Tag="{Binding BindsDirectlyToSource=True}"
                                                                Command="{Binding DataContext.Btn_AddNewDataModel_Click, RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor}}"
                                                                CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>

</DataTemplate>-->

User Control XAML

<UserControl x:Class="Nikola_Software_Installation_App.CustomControls.SoftwareInstallStatus_Ctrl"
             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:Nikola_Software_Installation_App.CustomControls"
             mc:Ignorable="d" 
             Name="SoftwareInstallStatusWindow"
             Height="{Binding WindowHeight,ElementName=SoftwareInstallStatusWindow, FallbackValue=30}" 
             Width="{Binding WindowWidth,ElementName=SoftwareInstallStatusWindow, FallbackValue=220}">
    <Grid>
        <Button x:Name="Btn_StartDownload"
                Content="{Binding ButtonContent,ElementName=SoftwareInstallStatusWindow, FallbackValue=Button}"
                Width="{Binding ButtonWidth,ElementName=SoftwareInstallStatusWindow, FallbackValue=40}"
                Margin="5,5,0,5" 
                HorizontalAlignment="Left"
                Tag="{Binding ButtonTag,ElementName=SoftwareInstallStatusWindow, FallbackValue=null}"/>
        <ProgressBar x:Name="Pgb_DownloadStatus"
                Width="{Binding ProgressBarWidth,ElementName=SoftwareInstallStatusWindow, FallbackValue=140}"
                Margin="50,5,0,5" 
                HorizontalAlignment="Left"  />
        <RadioButton x:Name="Rbn_Install"
                Margin="200,8,0,8" 
                HorizontalAlignment="Left"/>
    </Grid>
</UserControl>

User Control.CS

using Infrastructure_Project.ViewModels;
using System.Windows;
using System.Windows.Controls;


namespace Nikola_Software_Installation_App.CustomControls
{
    public partial class SoftwareInstallStatus_Ctrl : UserControl
    {
        #region "Window"

        public int WindowWidth
        {
            get { return (int)GetValue(WindowWidthProperty); }
            set { SetValue(WindowWidthProperty, value); }
        }
        public static readonly DependencyProperty WindowWidthProperty =
            DependencyProperty.Register("WindowWidth", typeof(int), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(220));

        public int WindowHeight
        {
            get { return (int)GetValue(WindowHeightProperty); }
            set { SetValue(WindowHeightProperty, value); }
        }
        public static readonly DependencyProperty WindowHeightProperty =
            DependencyProperty.Register("WindowHeight", typeof(int), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(30));

        #endregion

        #region "Button"

        public string ButtonContent
        {
            get { return (string)GetValue(ButtonContentProperty); }
            set { SetValue(ButtonContentProperty, value); }
        }
        public static readonly DependencyProperty ButtonContentProperty =
            DependencyProperty.Register("ButtonContent", typeof(string), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(string.Empty));


        public int ButtonWidth
        {
            get { return (int)GetValue(ButtonWidthProperty); }
            set { SetValue(ButtonWidthProperty, value); }
        }
        public static readonly DependencyProperty ButtonWidthProperty =
            DependencyProperty.Register("ButtonWidth", typeof(int), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(100));


        public object ButtonTag
        {
            get { return (object)GetValue(ButtonTagProperty); }
            set { SetValue(ButtonTagProperty, value); }
        }
        public static readonly DependencyProperty ButtonTagProperty =
            DependencyProperty.Register("ButtonTag", typeof(object), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(null));



        #endregion

        #region "ProgressBar"

        public int ProgressBarWidth
        {
            get { return (int)GetValue(ProgressBarWidthProperty); }
            set { SetValue(ProgressBarWidthProperty, value); }
        }
        public static readonly DependencyProperty ProgressBarWidthProperty =
            DependencyProperty.Register("ProgressBarWidth", typeof(int), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(140));

        #endregion

        public SoftwareInstallStatus_Ctrl()
        {
            InitializeComponent();
            DataContext = new SoftwareInstallStatus_ViewModel();
        }
    }
}
Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,668 questions
{count} votes

Accepted answer
  1. Emon Haque 3,176 Reputation points
    2021-06-20T15:13:36.4+00:00

    Your Custom Control can be something like this:

    class SoftwareInstallStatus_Ctrl : Grid  
    {  
        Button button;  
        ProgressBar progress;  
        RadioButton radio;  
        public SoftwareInstallStatus_Ctrl() {  
            button = new Button();  
            progress = new ProgressBar() { Margin = new Thickness(5,0,5,0)};  
            radio = new RadioButton();  
            SetColumn(progress, 1);  
            SetColumn(radio, 2);  
            ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(150) });  
            ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });  
            ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });  
            Children.Add(button);  
            Children.Add(progress);  
            Children.Add(radio);  
        }  
        protected override void OnInitialized(EventArgs e) {  
            button.SetBinding(Button.ContentProperty, new Binding(nameof(ButtonContent)) { Source = this });  
            progress.SetBinding(ProgressBar.ValueProperty, new Binding(nameof(ProgressValue)) { Source = this });  
            radio.SetBinding(RadioButton.IsCheckedProperty, new Binding(nameof(IsChecked)) { Source = this });  
        }  
    
        public string ButtonContent {  
            get { return (string)GetValue(ButtonContentProperty); }  
            set { SetValue(ButtonContentProperty, value); }  
        }  
        public double ProgressValue {  
            get { return (double)GetValue(ProgressValueProperty); }  
            set { SetValue(ProgressValueProperty, value); }  
        }  
        public bool IsChecked {  
            get { return (bool)GetValue(IsCheckedProperty); }  
            set { SetValue(IsCheckedProperty, value); }  
        }  
    
        public static readonly DependencyProperty IsCheckedProperty =  
            DependencyProperty.Register("IsChecked", typeof(bool), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(false));  
    
        public static readonly DependencyProperty ProgressValueProperty =  
            DependencyProperty.Register("ProgressValue", typeof(double), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(0d));  
    
        public static readonly DependencyProperty ButtonContentProperty =  
            DependencyProperty.Register("ButtonContent", typeof(string), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(null));  
    }  
    

    With this approach you don't need anything else for Custom control. In your MainWindow.xaml, you can have these:

    <Grid Margin="20">  
        <ItemsControl ItemsSource="{Binding Software}">  
            <ItemsControl.ItemTemplate>  
                <DataTemplate>  
                    <local:SoftwareInstallStatus_Ctrl   
                        ButtonContent="{Binding Name}"  
                        ProgressValue="{Binding Progress}"  
                        IsChecked="{Binding IsComplete}"/>  
                </DataTemplate>  
            </ItemsControl.ItemTemplate>  
        </ItemsControl>  
    </Grid>  
    

    and in MainWindow.xaml.cs these:

    public partial class MainWindow : Window  
    {  
        public List<Soft> Software { get; set; }  
        public MainWindow() {  
            InitializeComponent();  
            Software = new List<Soft>();  
            Software.Add(new Soft() { Name = "Rent Manager", Progress = 100, IsComplete = true });  
            Software.Add(new Soft() { Name = "Stock Trader", Progress = 75, IsComplete = false });  
            Software.Add(new Soft() { Name = "Something Else", Progress = 80, IsComplete = false });  
            DataContext = this;  
        }  
    }  
    public class Soft  
    {  
        public string Name { get; set; }  
        public double Progress { get; set; }  
        public bool IsComplete { get; set; }  
    }  
    

    you'll see this when you launch the app:

    107332-capture.png

    In my apps, I used FrameworkElement, ArrangeOverride, MeasureOverride, VisualCollection, etc. BUT when you need some sort of container like Grid, StackPanel, Border, etc. You simply could subclass the container/control and add relevant controls, dependency/normal properties and bind.

    EDIT
    ---
    For command, nowadays, I don't use ICommand, I use Action instead. The approach with Action is add two more dependency properties in your custom control:

    public Action<object> Command {  
        get { return (Action<object>)GetValue(CommandProperty); }  
        set { SetValue(CommandProperty, value); }  
    }  
    public object CommandParameter {  
        get { return (object)GetValue(CommandParameterProperty); }  
        set { SetValue(CommandParameterProperty, value); }  
    }  
    public static readonly DependencyProperty CommandParameterProperty =  
        DependencyProperty.Register("CommandParameter", typeof(object), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(null));  
    
    public static readonly DependencyProperty CommandProperty =  
        DependencyProperty.Register("Command", typeof(Action<object>), typeof(SoftwareInstallStatus_Ctrl), new PropertyMetadata(null));  
    

    and override another Preview function in your custom control:

    protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) {  
        if(e.Source is Button)   
            Command.Invoke(CommandParameter);  
    }  
    

    and in the viewModel add an Action Property and initialize like this:

    ...  
    public Action<object> TheCommand { get; set; }  
    public MainWindow() {  
        ...  
        TheCommand = execute;  
        DataContext = this;  
    }  
    void execute(object o) {  
        Debug.WriteLine(o);  
    }  
    

    and in MainWindow.xaml Bind those like this:

    <ItemsControl ItemsSource="{Binding Software}" x:Name="items">  
        <ItemsControl.ItemTemplate>  
            <DataTemplate>  
                <local:SoftwareInstallStatus_Ctrl   
                    ButtonContent="{Binding Name}"  
                    Command="{Binding DataContext.TheCommand, Source={x:Reference items}}"  
                    CommandParameter="Hello"  
                    ProgressValue="{Binding Progress}"  
                    IsChecked="{Binding IsComplete}"/>  
            </DataTemplate>  
        </ItemsControl.ItemTemplate>  
    </ItemsControl>  
    

    It'll work. If you want to use ICommand, make the DP a type of ICommand and use that instead, that'll work as well.

    EDIT
    ----
    For ICommand, add these in OnInitialized:

        protected override void OnInitialized(EventArgs e) {  
            button.SetBinding(Button.ContentProperty, new Binding(nameof(ButtonContent)) { Source = this });  
            button.SetBinding(Button.CommandProperty, new Binding(nameof(Command)) { Source = this });  
            button.SetBinding(Button.CommandParameterProperty, new Binding(nameof(CommandParameter)) { Source = this });  
            progress.SetBinding(ProgressBar.ValueProperty, new Binding(nameof(ProgressValue)) { Source = this });  
            radio.SetBinding(RadioButton.IsCheckedProperty, new Binding(nameof(IsChecked)) { Source = this });  
        }  
    

    you don't have to override that Preview function in Custom Control. In MainWindow.xaml.cs have these:

    public partial class MainWindow : Window  
    {  
        ...  
        public ICommand TheCommand { get; set; }  
        public MainWindow() {  
            ---  
            TheCommand = new Command(execute, (o) => true);  
            DataContext = this;  
        }  
        void execute(object o) {  
            Debug.WriteLine(o);  
        }  
    

    and in MainWindow.xaml these:

    <ItemsControl ItemsSource="{Binding Software}" x:Name="items">  
        <ItemsControl.ItemTemplate>  
            <DataTemplate>  
                <local:SoftwareInstallStatus_Ctrl   
                    ButtonContent="{Binding Name}"  
                    Command="{Binding DataContext.TheCommand, Source={x:Reference items}}"  
                    CommandParameter="{Binding Name}"  
                    ProgressValue="{Binding Progress}"  
                    IsChecked="{Binding IsComplete}"/>  
            </DataTemplate>  
        </ItemsControl.ItemTemplate>  
    </ItemsControl>  
    
    0 comments No comments

5 additional answers

Sort by: Most helpful
  1. Marc Jeeves 386 Reputation points
    2021-06-20T15:57:42.733+00:00

    So i can still do MVVM by setting the datacontext still right?


  2. Marc Jeeves 386 Reputation points
    2021-06-20T16:08:04.7+00:00

    Last Question I promise

    If I want to use a datacontext, how do I set the button command and parameter to my relay command in the viewmodel?

    my goal is that each button can be pressed on a separate background thread to download the install msi, once completed the user can select the radio button to run the .msi file.

    Thanks

    Madaxe

    public SoftwareInstallStatus()
            {
                DataContext = new SoftwareInstallStatus_ViewModel();
    
                button = new Button();
                button.CommandBindings.Add(new Binding(nameof(DataContext.Btn_AddNewDataModel_Click)) { Source = this.DataContext});
    
    0 comments No comments

  3. Marc Jeeves 386 Reputation points
    2021-06-20T16:39:55.68+00:00

    I tried adding a new commandbinding that was pointed at the viewmodels relay command but it still does not fire

    any thoughts?

    Thanks

    ViewModel

    public RelayCommand Btn_AddNewDataModel_Click { get; private set; }
    
    public SoftwareInstallStatus_ViewModel()
            {
                Btn_AddNewDataModel_Click = new RelayCommand(AddNewDataModel, CanAddNewDataModel);
            }
    
            public void AddNewDataModel(object message)
            {
                // I Was Clicked;
            }
            public bool CanAddNewDataModel(object message)
            {
                return true;
            }
    

    View

    protected override void OnInitialized(EventArgs e)
            {
                button.SetBinding(Button.ContentProperty, new Binding(nameof(ButtonContent)) { Source = this });
                progress.SetBinding(ProgressBar.ValueProperty, new Binding(nameof(ProgressValue)) { Source = this });
                radio.SetBinding(RadioButton.IsCheckedProperty, new Binding(nameof(IsChecked)) { Source = this });
    
                button.CommandBindings.Add(new CommandBinding(_SoftwareInstallStatus_ViewModel.Btn_AddNewDataModel_Click));
    
            }
    

  4. Marc Jeeves 386 Reputation points
    2021-06-20T17:36:26.95+00:00

    This is really new to me and a little complex, so forgive me for being a little slow on the uptake

    The XAML example i Understand since its binding to the Viewmodel defined by the datacontext, how do I do the same programmatically?

    I noticed there is not a Button.Command also.

    XAML
    Command="{Binding DataContext.Btn_AddNewDataModel_Click, RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor}}"

    .CS
    button.SetBinding(Button.CommandProperty, new Binding(nameof(Btn_AddNewDataModel_Click){ Source = this.DataContext});

    0 comments No comments