2

We have a custom-rendered ListBox which maintains an instance of a StreamGeometry object that is based on its width. The control needs to then share that StreamGeometry instance with all of its items for rendering purposes.

Only way we can think is putting that StreamGeometry instance in the ViewModel for the ListBox, then binding to it in the individual DataTemplates, which just feels dirty to me considering that is a view-only thing and therefore shouldn't be in the ViewModel at all.

Note: We could also just store it via an attached property on the ListBox (or subclass the ListBox), but we're still left with binding of a view-only thing which seems wrong to me for something like this.

Any thoughts?

Charles
  • 50,943
  • 13
  • 104
  • 142
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • What about `Window.Resources`? – Clemens Dec 03 '12 at 22:39
  • No, because its ListBox-instance specific and there can be many. Plus, it's dictated by properties of the ListBox. My actual question is more around the actual sharing to the items themselves through the data template. I mean is that even the right way to do it? I thought binding could be slow. – Mark A. Donohoe Dec 03 '12 at 22:41
  • An object (e.g. a PathGeometry if that is what you mean by GraphicsPath) in a resource dictionary can be shared by as many ListBoxItems as you like. – Clemens Dec 03 '12 at 22:43
  • Clemens, as I said, it's per-instance of a ListBox, meaning each ListBox has to maintain its own instance. The question is more along the lines of the most efficient way to get that StreamGeometry instance from the ListBox (or the ViewModel for it) to the individual item template instances. Binding of a StreamGeometry property just seems like it will be slow, but I could be wrong. – Mark A. Donohoe Dec 03 '12 at 22:45
  • 2
    Then what exactly do you mean by "share" when each ListBoxItem has has own instance? Maybe you provide some example XAML? – Clemens Dec 03 '12 at 22:47
  • Each ListBox has to maintain its own instance. When the ListBox resizes, it needs to recreate the StreamGeometry since it is based on the size of the ListBox. That instance needs to be shared with all of that ListBox's ListBoxItem instances since that's where the actual rendering takes place. Hopefully that clarifies things. – Mark A. Donohoe Dec 03 '12 at 22:49
  • I've edited the question for clarity. – Mark A. Donohoe Dec 03 '12 at 22:55

1 Answers1

0

You can make the StreamGeometry a dependency property on your custom listview, then refer to it through Binding MyGeometry, RelativeSource={RelativeSource AncestorType=ListView}.

This way, there is no ViewModel involved.

enter image description hereenter image description here

Xaml:

<Window x:Class="WpfApplication1.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:local="clr-namespace:WpfApplication1"
                xmlns:s="clr-namespace:System;assembly=mscorlib"
                Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <!-- default lsitviewitem style except for added path -->
        <Style TargetType="{x:Type ListViewItem}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Padding" Value="2,0,0,0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListViewItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <StackPanel Orientation="Horizontal">
                                <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                                <!-- added path-->
                                <Path Stretch="Uniform" Stroke="DarkBlue" Fill="DarkOrchid" Data="{Binding MyGeometry, RelativeSource={RelativeSource AncestorType=ListView}}" />
                            </StackPanel>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsSelected" Value="true"/>
                                    <Condition Property="Selector.IsSelectionActive" Value="false"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid >
        <local:CustomListView Margin="20" >
            <local:CustomListView.Items>
                <ListViewItem Content="ListViewItem1" />
                <ListViewItem Content="ListViewItem2" />
                <ListViewItem Content="ListViewItem3" />
            </local:CustomListView.Items>
        </local:CustomListView>
    </Grid>
</Window>

CustomListView:

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfApplication1
{
    public class CustomListView : ListView
    {
        public StreamGeometry MyGeometry { get { return (StreamGeometry)GetValue(MyGeometryProperty); } set { SetValue(MyGeometryProperty, value); } }
        public static readonly DependencyProperty MyGeometryProperty = DependencyProperty.Register("MyGeometry", typeof(StreamGeometry), typeof(CustomListView), new PropertyMetadata(null));

        protected override void OnRender(DrawingContext drawingContext)
        {
            StreamGeometry geometry = new StreamGeometry(); // directly opening MyGeometry results in "must have isfrozen set to false to modify" error
            using (StreamGeometryContext context = geometry.Open()) 
            {
                Point p1 = new Point(this.ActualWidth * (2d / 5d), 0);
                Point p2 = new Point(this.ActualWidth / 2d, -10);
                Point p3 = new Point(this.ActualWidth * (3d / 5d), 0);

                context.BeginFigure(p1, true, true);

                List<Point> points = new List<Point>() { p2, p3 };
                context.PolyLineTo(points, true, true);
            }

            drawingContext.DrawGeometry(Brushes.DarkOrchid, new Pen(Brushes.DarkBlue, 1), geometry);

            this.MyGeometry = geometry;

            base.OnRender(drawingContext);
        }
    }
}
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • That's similar to what I mentioned in the question; the only difference being I was saying to use an attached property whereas you went with a subclass. Also, we don't want to use a binding like that because we do custom rendering in the ListBoxItems that uses the path, whereas you use the visual tree, which I believe* clutters up the UI (our path is very complex.) (I put that asterisk because under the hood, the rendering may actually be building up the visual tree itself, meaning my statement is only partially right. More research on my part is needed there.) – Mark A. Donohoe Mar 22 '13 at 20:06
  • Looks like I have trouble getting what you're aiming at. I'm trying to understand: When you say you are custom rendering your listbox, that doesn't entail a subclass, so you're not overriding the OnRender method? What means "cluttering up" the UI, slow to react? I reckon with my method, there's one shared path instance. You can bind it to a property of a CustomListViewItem if you want to custom render it instead of directly putting it in the visual tree. As for the binding, I definitely don't understand your concern. This is binding to a dp of another visual, nothing to do with Data/ViewModel! – Mike Fuchs Mar 23 '13 at 00:45
  • We're doing custom rendering in a sublcass of ListBoxItem which is applied via `ItemContainerStyle`. We however want every ListBoxItem *for a single instance of a listbox* to share a path, so naturally the ListBox would have to hold that path's instance. I was just wondering if there was a good way to do that. Your way would work, the same as an attached property. I just hate the thought of using a binding to get to it, let alone with a RelativeSource, but I don't know any other way for the ListBoxItem to get to it. (Then again, I think it does have some reference to the ItemsControl) – Mark A. Donohoe Mar 23 '13 at 04:01
  • Well, you can get to the StreamGeometry within a CustomListViewItem by `(this.Parent as CustomListView).MyGeometry`, but you will just end up trying to manufacture your own implementation of a binding this way, in my opinion. – Mike Fuchs Mar 23 '13 at 12:09
  • Actually, from another post I made (http://stackoverflow.com/questions/14118414/why-does-the-parent-property-of-a-container-of-an-itemscontrol-return-null-and-n) the parent of a container (i.e. ListBoxItem, ComboBoxItem, etc.) returns null, not the ItemsControl itself, so this doesn't work either. I agree it should as that would be very simple (and wouldn'd be a binding at all! Much more efficient!), but the powers that be at MS apparently disagree. Crazy if you ask me. – Mark A. Donohoe Mar 23 '13 at 19:37
  • I debugged my code and I get the CustomListView in the parent property (even if I apply the ListViewItem style through ItemContainerStyle), not null. What else would I have to change to reproduce this problem? Also, I would like to read up on why you say binding is inefficient, as I am not aware of that, any good resource? – Mike Fuchs Mar 23 '13 at 20:10
  • I never said binding is inefficient, but it is not *as* efficient as direct access since it relies on change notification, which direct access does not. But using RelativeSource is even slower since it has to walk the hierarchy when binding, which isn't a one-time setup when you're dealing with a virtualized ItemsControl. It happens every time the container is recycled when scrolling. As for a test, try it with a ListBox, then use Snoop to look at the ListBoxItem objects. Perhaps that issue doesn't exist with the ListView control. – Mark A. Donohoe Mar 23 '13 at 20:32