ChrisOBrien
ChrisOBrien

Reputation: 61

WPF Data binding problems for Itemscontrol with Canvas as ItemsPanel

I am having a problem with binding I am trying to bind a list of items to an items control. I do have it working but I am getting some binding errors that depend on how WidgetVM is defined.

If WidgetVM inherits from thumb (which is what I want), it wont appear on the canvas. the same is for any other derivation of control. Now the three items defined in the constructor are in the Live visual tree at the position but he Canvas.Left and Canvas .Top Property is NaN, which I guess is why they are not showing

Also there are binding errors stating the Position Property was not found on type MainWindow? I cannot figure out why it looks for the values in MainWindow and not WidgetVM. these errors dissapear when I remove the inheritance from Thumb.

If I remove the inheritance from WidgetVM the text values appear as expected on screen so the code to place the items works but not when the object is derived from a control such as thumb.

How can I get this to work when WidgetVM inherits from Thumb as I want to use the drag delta functionality. Thanks.

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <TextBlock Text="{Binding Path=WidgetName}"/>
    </DataTemplate>
</ItemsControl.ItemTemplate>

<ItemsControl.ItemContainerStyle>
    <Style>
        <Setter Property="Canvas.Left" Value="{Binding Path=Position.X}"/>
        <Setter Property="Canvas.Top" Value="{Binding Path=Position.Y}"/>
    </Style>
</ItemsControl.ItemContainerStyle>

The Code Behind is as follows

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string v)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(v));
    }


    private ObservableCollection<Widgetvm> Widgets;
    public ObservableCollection<Widgetvm> Widgets
    {
        get { return Widgets; }
        set { Widgets = value; OnPropertyChanged("Widgets"); }
    }

public MainWindow()
{
    Widgets = new ObservableCollection<Widgetvm>()
    {
        new Widgetvm (){WidgetName="initial", Position = new Point(30,30) },
        new Widgetvm (){WidgetName="inbetween", Position = new Point(120,130) },
        new Widgetvm (){WidgetName="final", Position = new Point(330,330) },
    };
    InitializeComponent();
}
}

The Widgetvm is defined as follows

public class Widgetvm : Thumb, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string v)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(v));
    }

    private string WidgetName;
    public string WidgetName
    {
        get { return WidgetName; }
        set { WidgetName = value; OnPropertyChanged("WidgetName"); }
    }

    private Point position;
    public Point Position
    {
        get { return position; }
        set { position = value; OnPropertyChanged("Position"); }
    }
}

Upvotes: 0

Views: 414

Answers (1)

BionicCode
BionicCode

Reputation: 29028

This is not how it works. When you add items to the ItemsSource then the DataTemplate is rendered and not the data item (Widgetvm). In other words, in your code only the TextBlock is displayed and no Thumb (because Thumb is the data item).
If you want do show the Thumb then you must add it to the DataTemplate. In case you need to implement additional logic for the Thumb, create a new control named Widget that extends Thumb.

  • Also use a ListBox instead of the ItemsControl.
  • Use nameof instead of strings to use member names as argument.
  • Don't implement INotifyPropertyChanged on controls. Impement properties as DependencyProperty instead. This allows the property to be a binding target or animated. It also improves the perfomance of your application. Generally, use DependecyProperty for classes that extend DependecyObject and INotifyPropertyChanged for all other types.

The following example shows how to create the draggable control Widget, that extends Thumb and is positioned on a Canvas:

WidgetItem.cs
The data model.

public class WidgetItem : INotifyPropertyChanged
{
  protected virtual void OnPropertyChanged([CallerMembername] string propertyName = "")
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

  public event PropertyChangedEventHandler PropertyChanged;

  private string name;
  public string Name
  {
    get => this.name; 
    set 
    { 
      this.name = value; 
      OnPropertyChanged(); 
    }
  }

  private Point position;
  public Point Position
  {
    get => this.position; 
    set 
    { 
      this.position = value; 
      OnPropertyChanged(); 
    }
  }
}

Widget.cs
The draggable control.
Create a control library or use a existing one and add the following control. The only important key is that your project has a Themes folder that must contain the Generic.xaml file.

public class Widget : Thumb
{
  public string Name
  {
    get => (string)GetValue(NameProperty);
    set => SetValue(NameProperty, value);
  }

  public static readonly DependencyProperty NameProperty =  DependencyProperty.Register(
    "Name", 
    typeof(string), 
    typeof(Widget), 
    new PropertyMetadata(default));

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

  // TODO::Implement UI logic and handle Thumb events here
}

The drag part is not implemented, TODO:

  1. add a dependency property X
  2. add a dependency property Y
  3. listen to the drag events and set X and Y accordingly
  4. in the DataTemplate of the ListBox, bind the WidgetItem.Position to X and Y

Generic.xaml

<Style TargetType="Widget">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Widget">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">

          <!-- Define visual appearance here. For example add a Rectangle and/or 
               a TextBlock to display the Name  -->

          <TextBlock Text="{TemplateBinding Name}" />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
  

MainWindow.xaml.cs
Define the data source.

public partial class MainWindow : Window
{
  public ObservableCollection<Widgetvm> WidgetItems { get; }

  public MainWindow()
  {
    InitializeComponent();

    this.DataContext = this;

    this.WidgetItems = new ObservableCollection<WidgetItem>
    {
      new WidgetItem(){ Name="initial", Position = new Point(30,30) },
      new WidgetItem(){ Name="inbetween", Position = new Point(120,130) },
      new WidgetItem(){ Name="final", Position = new Point(330,330) },
    };
  }
}

MainWindow.xaml

<Window>
  <ListBox ItemsSource="{Binding WidgetItems}">
    <ListBox.ItemPanel>
      <!-- Set to Canvas -->
    <ListBox.ItemPanel>

    <ListBox.ItemTemplate>
      <DataTemplate DataType="{x:Type WidgetItem}">
        <Widget Name="{Binding Name}" />
      </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemContainerStyle>
      <Style TargetType="ListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding Position.X}"/>
        <Setter Property="Canvas.Top" Value="{Binding Position.Y}"/>
      </Style>
    </ListBox.ItemContainerStyle>
  </ListBox>
</Window>

Upvotes: 1

Related Questions