We're trying to add "block select" to the Silverlight DataGrid control: The user should be able to select, for example, a rectangle of cells from ( col 4, row 5 ) to ( col 6, row 8 ).
What we're doing is saving the two corners of the selection, and indicating it visually by setting the background color of the cells. We've run into trouble with scrolling, because the cell objects are recycled, along with their formatting. So you scroll up, and as the selected cells vanish off the bottom, bars of cells coming in at the top are colored! I've tried saving a List of the actual cell objects and the "new" colored cells are definitely the same DataGridCell instances, though with different content of course.
We're able to get our hands on the scrollbars via the visual tree, so we'll may end up refreshing the selection display in a ValueChanged event handler for the vertical scrollbar.
But I'd like to know if there's a better way. We're not Silverlight experts. Has anybody tried to do this? Is there anything obvious to a Silverlight whiz that we're not even thinking of?
We're not going to buy anything. For corporate bureaucracy reasons, unfortunately, that's not an option.
Why not include that in you viewmodel. What i would do is create a nested enumerable viewmodel of the interaction, ie if the datagrid is bound to a IEnumerable of T where T is a viewmodel representing each row, id have something like IndexSelected on that viewmodel.
Then id bind the back color using a valueconverter of some sort to that indexselected property,
public class RowViewModel
{
public string Col1 { get; set; }
public string Col2 { get; set; }
public string Col3 { get; set; }
public int IndexSelected { get; private set; }
//Id also make a command here or something to set the indexselected but ill leave that for you :)
}
public class GridViewModel
{
public ObservableCollection<RowViewModel> Rows; // Bound to Datagrid.ItemsSource.
}
Notice the converter param on the indexselected binding holds the index of the column
<sdk:DataGrid>
<sdk:DataGrid.Columns>
<sdk:DataGridTemplateColumn Header="Col1">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid Background="{Binding IndexSelected, Converter={StaticResource IndexToColorConverter}, ConverterParameter=1}">
<TextBlock Text="{Binding Col1}" />
</Grid>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn Header="Col2">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid Background="{Binding IndexSelected, Converter={StaticResource IndexToColorConverter}, ConverterParameter=2}">
<TextBlock Text="{Binding Col2}" />
</Grid>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
and all the converter will do is check if the indexselected bound property equals the parameter (which is the index of the column)
public class IndexToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == parameter)
{
return new SolidColorBrush(Colors.Red);
}
return new SolidColorBrush(Colors.White);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Related
I would like to have my ListBox layout like this:
But currently I have this:
And my xml code looks like this:
<ListBox DockPanel.Dock="Top" Grid.Column="1" Grid.Row="1" Height="200" ScrollViewer.VerticalScrollBarVisibility="Visible">
<ListBoxItem Width="325" Content="Visual Basic"/>
<ListBoxItem Width="325" Content="Silverlight"/>
<ListBoxItem Width="325" Content="ASP.NET"/>
<ListBoxItem Width="325" Content="WCF"/>
<ListBoxItem Width="325" Content="Web Services"/>
<ListBoxItem Width="325" Content="Windows Service"/>
</ListBox>
So how can I make two columns in a ListBox and add data to them?
Here is an inspiration how you could display your enthusiasm for boats. This is a simple example in code-behind, as I do not know how deep your knowledge about WPF is.
First, create a data type that represents a book with a Title and an Authors property.
public class Book
{
public Book(string title, IEnumerable<string> authors)
{
Title = title;
Authors = authors;
}
public string Title { get; }
public IEnumerable<string> Authors { get; }
}
Then in your window's code-behind, expose a collection for your books and initialize it in the constructor. Set the DataContext to this, so bindings will have the window as source and find the Books property.
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
Books = new List<Book>
{
new Book("Boaty McBoatface", new[] { "Byron Barton" }),
new Book("Boat life", new[] { "Jeremy Boats", "Bud Shipman" }),
new Book("Boat Boys", new[] { "Will Buy Boats" }),
};
DataContext = this;
}
public IEnumerable<Book> Books { get; }
}
Now, create a ListView that binds its ItemsSource to the Books property. Remember, the binding will resolve the property on the window, as we set it as DataContext.
Add a GridView to the View property of the ListView. A GridView is responsible for displaying the columns, which you have to define. A GridViewColumn has a DisplayMemberBinding which can be bound to any property within the item type of the bound collection, here Book. If you bind a property like this, the value will be displayed as plain text in a column cell. For custom data types, you can specify a DataTemplate as CellTemplate.
<ListView ItemsSource="{Binding Books}">
<ListView.Resources>
<local:StringsToCommaSeparatedStringConverter x:Key="StringsToCommaSeparatedStringConverter"/>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn Header="Title"
DisplayMemberBinding="{Binding Title}"/>
<GridViewColumn Header="Authors"
DisplayMemberBinding="{Binding Authors, Converter={StaticResource StringsToCommaSeparatedStringConverter}}"/>
</GridView>
</ListView.View>
</ListView>
For the collection of authors we use another techique in WPF, value converters. Those can be used in binding and will convert a value from the source to something else for the target to display and vice-versa. Here, I created a custom value converter, that converts a collection of strings to a comma-separted single string.
public class StringsToCommaSeparatedStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is IEnumerable<string> values))
return Binding.DoNothing;
return string.Join(", ", values);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("This converter is not implemented two-way, because of lazyness.");
}
}
As you saw in the XAML above, you need to create an instance of the converter in any resources in scope and specify it in the binding where you want to apply it.
This is a more or less simple example to start with. You should sooner or later start learning the MVVM design pattern to improve separation of the user interface and your data and business logic. To migrate this example to MVVM, you would move the books collection property to a view model that you would set as DataContext e.g. of the window.
Further reading:
GridView Overview
Data Templating Overview
WPF Apps With The Model-View-ViewModel Design Pattern
By the way, in general a ListBox does not have built-in support for columns, a ListView does through GridView. There is even a much more powerful but at the same time complex control for tabular data, the DataGrid, in case you ever need it. Keep on boating!
I'm implementing an ObservableCollection <-> DataGrid binding.
My class consists of a System.Windows.Media.Color field (named 'Color') and several strings.
My implementation in WPF of the Color column:
<DataGridTemplateColumn Header="Color" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Source={StaticResource ColorProperties}}"
SelectedItem="{Binding Path=Color, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource ComboBoxFlatStyle}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="1" Orientation="Horizontal">
<Rectangle Fill="{Binding}" Height="10" Width="10" Margin="2"/>
<TextBlock Text="{Binding}" Margin="2,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
And ColorProperties:
<ObjectDataProvider x:Key="ColorProperties" ObjectType="{x:Type color:ColorHelper}"
MethodName="GetColorNames"/>
The ColorHelper class (I'll try to find the OP for credit):
public static class ColorHelper
{
public static IEnumerable<string> GetColorNames()
{
foreach (PropertyInfo p
in typeof(Colors).GetProperties(
BindingFlags.Public | BindingFlags.Static))
{
yield return p.Name;
}
}
}
This works just fine, I can see the combo-box with all the colors:
Now I added a few objects to the ObservableCollection, and their respective fields are populated in the DataGrid. All except the Color.
The default Color is Black, but when I run the application, the ComboBox stays empty.
When I select a color from the ComboBox, the ObservableCollection changes accordingly. However if I change the object itself, the ComboBox stays with its original value.
For troubleshooting, I added
<DataGridTextColumn Header="Color" Binding="{Binding Color, UpdateSourceTrigger=LostFocus}"
Width="100"/>
This column gets populated with the string representation of the color, and when I change the value from either side, it changes accordingly.
This fact suggests that there's something missing in the ComboBox implementation.
Am I missing some sort of translation method? Seems weird since it actually works in one way (from ComboBox to object).
I've searched everywhere for a solution, without success.
EDIT:
I implemented Color<->String converter, still doesn't work..
public class StringToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Color content = (Color)value;
return content.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string content = (string)value;
return (Color)ColorConverter.ConvertFromString(content);
}
}
WPF change:
<ComboBox ItemsSource="{Binding Source={StaticResource ColorProperties}}"
SelectedItem="{Binding Path=Color, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay,
Converter={StaticResource StringToColorConverter}}"
Style="{StaticResource ComboBoxFlatStyle}">
Any ideas?
Thanks!
The list bound to Combobox is a list of strings, whereas the actual object is not just a string. you need a converter that converts from string to your desired object.
Rest all seems fine, system dosent know how does string Black translates to Color Black
-- one way binding works because, color object knows how to get color from a string, that is why when we set Background="String" it get the corresponding object using FromName
OK i found out what was wrong. It looks like getting the actual name from a color is not that simple. ToString returns the hex string, and not the actual color name.
So I used the answer from http://www.blogs.intuidev.com/post/2010/02/05/ColorHelper.aspx to create the converter.
public class ColorToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Color KnownColor;
Color content = (Color)value;
Type ColorType = typeof(System.Windows.Media.Colors);
PropertyInfo[] ColorsCollection = ColorType.GetProperties(BindingFlags.Public | BindingFlags.Static);
foreach (PropertyInfo pi in ColorsCollection)
{
KnownColor = (Color)pi.GetValue(null);
if (KnownColor == content)
return pi.Name;
}
return String.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
string content = (string)value;
return (Color)ColorConverter.ConvertFromString(content);
}
}
In my WPF application, I have a TabControl named ParentTabControl, which ContentTemplate is composed of several controls, including a TabControl named ChildTabControl.
ParentTabControl contains, say, 2 tabs (each one bound to a different source), where as ChildTabControl always contain 1 tab. I'm focusing the first tab of ParentTabControl. By default, the first (the only one) tab of ChildTabControl is selected. The problem is that, if I switch to the second tab of ParentTabControl, the tab of its ChildTabControl is not selected. Is that a normal behavior? How can I do to always select a tab?
I hope I'm clear enough. Here is some code:
ParentTabControl:
<TabControl Name="ParentTabControl"
ItemsSource="{Binding ParentItemsSource}"
ContentTemplate="{StaticResource ResourceKey=ContentTemplate}"
IsSynchronizedWithCurrentItem="True" />
ContentTemplate:
<DataTemplate x:Key="ContentTemplate">
<TabControl Name="ChildTabControl"
ItemsSource="{Binding ChildItemsSource, Converter={StaticResource ResourceKey=ItemToObservableCollectionConverter }}" />
</DataTemplate>
ItemsSource:
public ObservableCollection<ParentData> ParentItemsSource { get; set; }
public class ParentData
{
public ChildData ChildItemsSource { get; set; }
}
Converter:
public class ItemToObservableCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new ObservableCollection<object> { value };
}
}
Thank you.
First, if you have one and only one item, why do you want a TabControl? If you just want to have an element with a header, you can do that in your ContentTemplate.
In order to fix the issue you're seeing, however, have you tried setting SelectedIndex="0" on the ChildTabControl? That should force it to always be selected.
EDIT for other possible solution
Well, I was able to replicate this, but it only happens intermittently in my test.
I think the easiest way you can fix this is to set the SelectedItem of the child TabControl to the ChildItemsSource:
<TabControl Name="ChildTabControl"
ItemsSource="{Binding ChildItemsSource, Converter={StaticResource ResourceKey=ItemToObservableCollectionConverter }}"
SelectedItem="{Binding ChildItemsSource}" />
I have no idea what is causing this issue, but this definitely fixes it.
I'm trying to create a Listbox that displays text and an image as one item. The listbox items have the text and the ID of the image. The image ID can be used in a LINQ expression to get the container of the image URI.
basically, I have two lists. Each item in the first list is a key to retrieve a specific item in the second list. How can I get both pieces of data to display in one listbox?
EDIT FOR CLARITY:
the first list is a collection of message objects - ObservableCollection:
class Message
{
public String PosterID { get; set; }
public String Text { get; set; } //this isn't important, it is the message body
}
The second list is a collection of user profiles - ObservableCollection:
class User
{
public String UserID { get; set; }
public String ProfilePictureURI { get; set; }
}
To get the profile data of the poster of a message, you must take 'Message.PosterID' and use that as a 'key' to match 'User.UserID'.
The listbox in question is currently databound to ViewModel.Messages. In the listbox's data template, I print out 'Message.Text' successfuly, BUT I still need to retrieve 'User.ProfilePictureURI.'
I am being recommended to use a ValueConverter to convert 'Message.PosterID' to 'User.UserID.' But to make this conversion, I need to pass ViewModel.Users into the ValueConverter. I currently don't know how to do this.
I think you have two ways:
A. Use a binding converter to convert the image id by looking up your image id from the second list, then bind your ListBox to the list.
B. Create a wrapper class that acts as a ViewModel for your data, and bind the ListBox to the ViewModel object.
EDIT
About using the value converter:
If you could make the Users list static or use some Dependency Injection mechanism to obtain the Users list, then your value converter could easily do the conversion.
The only other way is somehow pass the Users list from ViewModel (DataContext) to the binding converter.
At first I thought that you can set the Users to a property of the converter, like this:
<ListBox ItemsSource="{Binding Path=Messages}">
<ListBox.Resources>
<c:PosterIDConverter x:Key="pidConverter" Users="..."/>
</ListBox.Resources>
...
Or pass it as ConverterParameter to the binding:
<TextBlock Text="{Binding Path=Text, Converter={StaticResource pidConverter,ResourceKey}, ConverterParameter=...}"/>
But how should I get the Users property from the DataContext? In any of the two above options, you should be able to bind the property to Users, like this (incorrect):
<c:PosterIDConverter x:Key="pidConverter" Users="{Binding Path=Users"/>
But PosterIDConverter is not DependencyObject and does not support data binding.
Working Solution:
Here is what I finally came to.
Your converter class:
public class PosterIDConverter : IMultiValueConverter
{
public object Convert(object[] value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string userId = (string)value[0];
IEnumerable<User> users= (IEnumerable<User>)value[1];
var user = users.FirstOrDefault(u => u.UserID == userId);
if (user != null)
return user.ProfilePictureURI;
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Your xaml file (just a sample):
<ListBox x:Name="lst" DataContext="{StaticResource vm}" ItemsSource="{Binding Path=Messages}">
<ListBox.Resources>
<c:PosterIDConverter x:Key="pidConverter"/>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type c:Message}">
<Border BorderBrush="Blue" BorderThickness="1">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Message: "/>
<TextBlock Text="{Binding Path=Text}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="URI: "/>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource pidConverter}">
<Binding Path="PosterID"/>
<Binding ElementName="lst" Path="DataContext.Users"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In this sample I bound the Text property of a TextBlock to the PosterID and Users property at the same time and used the value converter (IMultiValueConverter) to convert them to image URI.
For my Silverlight TreeView's items source I have a collection of Order items, and each Order has two collections, OrderItems and Commissions. So I want a treeview that looks kind of like
Order #1
- Order Items
- Order Item #1
- Order Item #2
- Order Item #3
- Commissions
- Commission #1
- Commission #2
- Commission #3
- Commission #4
etc. So ever Order will have a Order Items and Commissions header, and the contents of these are databound. I'm kind of stumped by this, even though it seems kind of simple.
This is the XAML I have so far. Obviously creating the HierarchicalDataTemplates for the OrderItems and CommissionsItems collections is simple, but how do I set the ItemsSource of the HDT above? In other words, what would [what goes here?] look like?
<Grid>
<Grid.Resources>
<sdk:HierarchicalDataTemplate
x:Key="OrdersTreeLevel0"
ItemsSource="{StaticResource [what goes here?]}"
ItemTemplate={StaticResource OrdersTreeLevel1}">
<TextBlock
FontWeight="{Binding IsUnread}"
Text="{Binding Id, StringFormat='Order #{0}'}" />
</sdk:HierarchicalDataTemplate>
</Grid.Resources>
<sdk:TreeView
ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource OrdersTreeLevel0}">
</sdk:TreeView>
</Grid>
You'll need a value converter for this to take an Order object and convert it to an array of a type that carries Name and Items properties:-
public class ItemWrapper
{
public string Name {get; set; }
public IEnumerable Items {get; set;}
}
public class OrderConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Order order = value as Order;
if (order != null)
{
List<object> result = new List<object>();
result.Add(new ItemWrapper() {Name = "Order Items", Items = order.OrderItems} );
result.Add(new ItemWrapper() {Name = "Commission Items", Items = order.CommissionItems} );
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Now you add this converter to your resources and reference it in your template:-
<Grid.Resources>
<local:OrderConverter x:Key="OrderConverter" />
<sdk:HierarchicalDataTemplate
x:Key="OrdersTreeLevel0"
ItemsSource="{Binding Converter=OrderConverter}"
ItemTemplate={StaticResource OrdersTreeLevel1}">
<TextBlock
FontWeight="{Binding IsUnread}"
Text="{Binding Id, StringFormat='Order #{0}'}" />
</sdk:HierarchicalDataTemplate>
</Grid.Resources>
Of course thats the easy bit, the really tricky bit is if you need a different template for Commission and OrderItem. If these both have simple properties in common such as Name then a single template for level2 will suffice. Otherwise things get more complicated.