question

MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 asked EmonHaque-1485 commented

WPF Custom Control Not Binding Correctly

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-wpf
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

In your UserControl try using RelativeSource binding. Binding ...., RelativeSource={RelativeSource TemplatedParent}, something like that, check out the web for exact syntax for this type of Binding, I used this approach when I first got into Custom Control, Now I'm in XAML-free world so I don't do those. You could subclass Control/FrameworkElement for Custom control. People use UserControls primarily for Views.

If you want to go with XAML approach for Custom Control, create a folder for Custom Controls, right click on that and go to Add -> New Item and from WPF tab, choose Custom Control. It'll add another folder Themes and there you'll have a .xaml file to define XAML and a .cs file in your folder to define dependency/normal properties and other logic. You could download the second project, RentManager with ICollectionView ..., from my GitHub to see a lot of these examples.

0 Votes 0 ·
EmonHaque-1485 avatar image
0 Votes"
EmonHaque-1485 answered EmonHaque-1485 edited

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>


capture.png (5.6 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 answered EmonHaque-1485 commented

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

· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Yes, you can.

0 Votes 0 ·
MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 answered MarcJeeves-9615 edited

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});
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 answered EmonHaque-1485 commented

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





· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

See the EDIT section of the answer.

0 Votes 0 ·

When you want to bind use SetBinding with Button.CommandProperty just like ContentProperty, not CommandBindings

0 Votes 0 ·
MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 answered

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



5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

MarcJeeves-9615 avatar image
0 Votes"
MarcJeeves-9615 answered EmonHaque-1485 commented

ok i found this which is great so my last problem is how to define the CommandParameter

thanks

Madaxe

 Binding binding = new Binding();
             binding.Path = new PropertyPath("Btn_AddNewDataModel_Click");
             button.SetBinding(Button.CommandProperty, binding);
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

See the last EDIT part it's both.

0 Votes 0 ·