views:

129

answers:

2

I have a question regarding how to best accomplish something in WPF MVVM. I have in my ViewModel a series of integers. For the sake of example, lets call them:

public int Yellow
{
    get;set;
}
public int Red
{
    get;set;
}
public int Green
{
    get;set;
}

I also have some small images that are very simple: A Red circle, a Yellow circle, and a Green circle. The idea is to have an area on the view with a number of these images, based on the above properties. So if this instance of the view model has 3 Yellow, 2 Red, and 1 Green, I want 6 images in my ListBox, 3 of the yellow circle, 2 of the red, and 1 of the green. Right now, I have it working, but using some very clumsy code where I build the image list in the ViewModel using an ugly for-loop. Is there some more elegant way to accomplish this task in WPF? Ideally, I wouldn't want to have to reference the image in the ViewModel at all...

+5  A: 

You could use an ImageBrush to tile a rectangle with an image, and bind the width of the rectangle to the number of copies of the image you want. Something like this:

<StackPanel Orientation="Horizontal">
    <StackPanel.LayoutTransform>
        <ScaleTransform ScaleX="20" ScaleY="20"/>
    </StackPanel.LayoutTransform>
    <Rectangle Width="{Binding Yellow}" Height="1">
        <Rectangle.Fill>
            <ImageBrush
                ImageSource="Yellow.png"
                Viewport="0,0,1,1"
                ViewportUnits="Absolute"
                TileMode="Tile"/>
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Width="{Binding Red}" Height="1">
        <Rectangle.Fill>
            <ImageBrush
                ImageSource="Red.png"
                Viewport="0,0,1,1"
                ViewportUnits="Absolute"
                TileMode="Tile"/>
        </Rectangle.Fill>
    </Rectangle>
    <Rectangle Width="{Binding Green}" Height="1">
        <Rectangle.Fill>
            <ImageBrush
                ImageSource="Green.png"
                Viewport="0,0,1,1"
                ViewportUnits="Absolute"
                TileMode="Tile"/>
        </Rectangle.Fill>
    </Rectangle>
</StackPanel>

Update: As Ray pointed out in his comment, if you are just trying to draw circles then you will get better zoom behavior by using a DrawingBrush than by using an Image:

<StackPanel Orientation="Horizontal">
    <StackPanel.LayoutTransform>
        <ScaleTransform ScaleX="20" ScaleY="20"/>
    </StackPanel.LayoutTransform>
    <StackPanel.Resources>
        <EllipseGeometry x:Key="Circle" RadiusX="1" RadiusY="1"/>
    </StackPanel.Resources>
    <Rectangle Width="{Binding Yellow}" Height="1">
        <Rectangle.Fill>
            <DrawingBrush ViewportUnits="Absolute" TileMode="Tile">
                <DrawingBrush.Drawing>
                    <GeometryDrawing
                        Brush="Yellow"
                        Geometry="{StaticResource Circle}"/>
                </DrawingBrush.Drawing>
            </DrawingBrush>
        </Rectangle.Fill>
    </Rectangle>
    <!-- etc. -->
Quartermeister
That looks like it would work. Are there any other solutions?
GWLlosa
Quartermeister's solution is essentially identical to my recommendation (+1). There are other solutions, but none so clean, simple, and elegant. Is there a reason you're dissatisfied with his answer?
Ray Burns
I did think of one possible improvement to Quartermeister's solution: You would get better zoom behavior if you used a DrawingBrush with a Drawing of a circle instead of an ImageBrush with a .png file. @Quartermeister: You're welcome to add this to your answer if you like.
Ray Burns
@Ray: Good point! He said that he had images so was thinking about image files, but if they really are just circles then DrawingBrush would be simpler. I'll update my answer.
Quartermeister
I wasn't so much dissatisfied with the answer as seeking to further my grasp on WPF in general. In my research I had come across several components that might work (ValueConverters, Triggers, Brushes, etc.) and was sorta hoping to get a bunch of different examples encompassing different techniques.
GWLlosa
A: 

A possibility would be to use a ValueConverter. It is very flexible, decoupled and helps to let the xaml simple. Here the code for such a value-converter:

public class ImageCountValueConverter : IValueConverter{
    public string ImagePath {
        get;
        set;
    }
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        if(null == value){
            return Enumerable.Empty<string>();
        } else if (value is int) {
            List<string> list = new List<string>();
            int v = (int)value;
            for (int i = 0; i < v; i++) {
                if (parameter is string) {
                    list.Add((string)parameter);
                } else {
                    list.Add(ImagePath);
                }
            }
            return list;
        } else {
            Type t = value.GetType();
            throw new NotSupportedException("The \"" + t.Name+ "\" type is not supported");
        }
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotImplementedException();
    }
}

The markup would look like this:

<StackPanel>
  <ItemsControl ItemsSource="{Binding Yellow,Converter={StaticResource ImageCount_ValueConverter},ConverterParameter=/image/yellow.png}" >
      <ItemsControl.ItemTemplate>
          <DataTemplate>
              <Image Source="{Binding}" Stretch="None"/>
          </DataTemplate>
      </ItemsControl.ItemTemplate>
  </ItemsControl>

  <ItemsControl ItemsSource="{Binding Red,Converter={StaticResource ImageCount_ValueConverter},ConverterParameter=/image/red.png}" >

  ...

The declaration would look something like:

  <Window.Resources>
        <local:ImageCountValueConverter x:Key="ImageCount_ValueConverter" ImagePath="/image/sampleImage.png"/>            
    </Window.Resources>

Options

Depending on your requirements you can also extend it or change it to work with ImageSource instead of strings or even provide a List<Brush> as output and then use a shape in your DataTemplate where the Brush is set through the Binding.

HCL