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!
Related
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.
In WPF you can use an IValueConverter or IMultiValueConverter to convert a data-bound value from say an int to a Color.
I have a collection of Model objects which I would like to convert to their ViewModel representations but in this scenario,
<ListBox ItemsSource="{Binding ModelItems,
Converter={StaticResource ModelToViewModelConverter}" />
the converter would be written to convert the whole collection ModelItems at once.
I wish to convert the items of the collection individually, is there a way to do that? I might want to use a CollectionViewSource and have some filtering logic so I don't want to have to iterate over the whole collection every time something changes.
You cannot set the converter on the collection itself, because it would get the collection as input. You have two choices:
Make sure your converter can also deal with collections (IEnumerable).
Use the converter within the item template.
If you want to use the second approach, then use something like this:
<ListBox ItemsSource="{Binding ModelItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Converter={StaticResource ModelToViewModelConverter}}"
ContentTemplate="{StaticResource MyOptionalDataTemplate}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
If you don't need a custom datatemplate, then you can skip the ContentTemplate attribute.
Yes you can. It is acting the same as with the IValueConverter. You simply treat the value parameter for the Convert method as a collection.
Here is an example of Converter for a collection:
public class DataConvert : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
ObservableCollection<int> convertible = null;
var result = value as ObservableCollection<string>;
if (result != null)
{
convertible = new ObservableCollection<int>();
foreach (var item in result)
{
if (item == "first")
{
convertible.Add(1);
}
else if (item == "second")
{
convertible.Add(2);
}
else if (item == "third")
{
convertible.Add(3);
}
}
}
return convertible;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this case is just a proof of concept, but I think it should show the idea very well.
The Converter converts from a simple collection of strings like this:
ModelItems = new ObservableCollection<string>();
ModelItems.Add("third");
ModelItems.Add("first");
ModelItems.Add("second");
into a collection of integers corresponding to the string meaning.
And here is the corresponding XAML (loc is the reference of the current assembly where is the converter):
<Window.Resources>
<loc:DataConvert x:Key="DataConverter"/>
</Window.Resources>
<Grid x:Name="MainGrid">
<ListBox ItemsSource="{Binding ModelItems, Converter={StaticResource DataConverter}}"/>
</Grid>
If you want to make a two way binding, you have to implement also the convert back. From the experience of working with MVVM, i suggest to use something like the Factory Pattern to transform from Model in ViewModel and backwards.
Here is another example. I'm using MVVM Caliburn Micro. MyObjects is a list of enums in my case.
<ListBox x:Name="MyObjects">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={StaticResource MyConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
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();
}
}
I am very new to WPF and testing some things that I would like to include in an application that I will be working on. I have a 2 row ListView (bound to a textbox) with the names Scott Guthrie and Jon Skeet in it. I am trying to select "Scott Guthrie" in the ListView and have it populate the TextBox. I want to be able to edit the text and tab off and have the ListView updated.
Edit:I removed the code since that really didn't add anything to the question.
Wow, that's really complicated what you've got there.
This can be accomplished in a very simple way. You need a model to represent the programmer, a view model to hold a list of programmers, and simple binding to take care of the rest.
The model:
public sealed class Programmer
{
public string Name { get; set; }
}
Its very simple. An object representing a programmer with a name. We must encapsulate the name within an object because strings are immutable in .NET. If you tried binding against a single string in a list of strings, changes wouldn't propagate.
The collection of programmers is kept in a ViewModel. In this case, I call it ViewModel, because I have no imagination. This view model contains everything that the view binds against. In this case, its the list of programmers.
public sealed class ViewModel
{
public ObservableCollection<Programmer> Programmers { get; private set; }
public ViewModel()
{
Programmers = new ObservableCollection<Programmer>();
}
}
The ViewModel is set as the DataContext of our view. The DataContext flows down the visual tree, and we can bind against it at any point.
public MainWindow()
{
var vm = new ViewModel();
vm.Programmers.Add(new Programmer { Name = "Jon Skeet" });
vm.Programmers.Add(new Programmer { Name = "Scott Guthrie" });
DataContext = vm;
InitializeComponent();
}
You can set the DataContext in any way you want; I'm doing it here for simplicity's sake.
In the UI, I simply bind the ListView against the list of Programmers in the ViewModel (the DataContext, unless otherwise stated, is the root of the binding path). I then bind the TextBox against the SelectedItem of the ListBox. You select a Programmer from the list, which then becomes the SelectedItem, which I can then change the Name of.
<Window
x:Class="Programmers.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:t="clr-namespace:Programmers"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox
x:Name="list"
ItemsSource="{Binding Programmers}"
DisplayMemberPath="Name" />
<TextBox
Grid.Column="1"
VerticalAlignment="Top"
Text="{Binding SelectedItem.Name, ElementName=list}" />
</Grid>
</Window>
Simple, once you get the hang of it.
This works (except that you need to validate the textbox since you can enter any text.. a dropdown might be a better choice).
View:
<TabItem x:Name="RightTabPage" Header="RightModel" DataContext="{Binding Right}">
<StackPanel>
<TextBox Text="{Binding SelectedGuru}"/>
<ListView SelectedItem="{Binding SelectedGuru}" ItemsSource="{Binding Gurus}"/>
</StackPanel>
</TabItem>
ViewModel:
public class RightViewModel
{
public RightViewModel()
{
Gurus = new[] {"Scott Guthrie", "Jon Skeet"};
SelectedGuru = Gurus.First();
}
public string SelectedGuru { get; set; }
public IEnumerable<string> Gurus{ get; set; }
}
Is there a way to limit the number of rows that get displayed in items control. ?
I have a observable collection of strings which are bound to Items control. I want to limit the number of rows to display to only one. The collection can have more than one.
Thanks,
You can use a IValueConverter for this:
public class ItemsLimiter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
int count;
if (Int32.TryParse((string)parameter, out count))
{
return ((IEnumerable<object>)value).Take(count);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return null;
}
}
In XAML you can use it like this:
<ItemsControl ItemsSource="{x:Bind Data, Converter={StaticResource ItemsLimiter}, ConverterParameter=12}">
Let's say your ItemsSource is set to MyObservableCollection.
Well, what if you change your ItemsSource so that it is pointing a MyOneItemCollection instead?
Then, just use LINQ to do something like this:
using System.Linq;
MyOneItemCollection = MyObservableCollection.First();
or
using System.Linq;
MyOneItemCollection = MyObservableCollection.Single(item => item.Id = MyId);
If you only ever need one item to display you can show the first item using a ContentControl instead with the same available templating options:
<ContentControl DataContext="{erm:Items FieldName}" Content="{Binding [0]}">
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="14" VerticalAlignment="Center" FontWeight="Bold" />
</DataTemplate>
</ContentControl>
Check out my PaginatedObservableCollection here http://jobijoy.blogspot.com/2008/12/paginated-observablecollection.html
This is a subclassed observableCollection which lets you bind to N items and make the UI display 'n' items when you set ItemsPerPage. In your case if you put 1 and can bind the next and previous also to some buttons as in my sample.
Hope this give you some idea.
You can implement a custom CollectionView that only provides n elements. I did something similar in my autocomplete textbox control implementation, check it out here:
A Reusable WPF Autocomplete TextBox
Scroll down to the header titled Limiting the Completions List see what I did there.
You can do this by creating a CollectionView that produces only a single item. This is quite simple: Just subclass CollectionView and override OnCollectionChanged to set a filter to filter out everything except the first item in the underlying collection:
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
var first = SourceCollection.FirstOrDefault();
Filter = (obj) => obj == first;
}
Now you need to insert this CollectionView into your ItemsSource. If erm:Items's ProvideValue produces the actual collection or a Binding with no Converter, you can just create your own MarkupExtension which either wraps it in your custom view or adds a Converter to do the wrapping. On the other hand, if erm:Items produces a Binding that already has a Converter or you can't rely on knowing what it produces, you should probably use a more general solution - I would suggest attached properties.
To use attached properties, your ItemsControl will be bound like this:
<ItemsControl
my:SinglerCollectionViewCreator.ItemsSource="{erm:Items FieldName}"
... />
and the code in the SinglerCollectionViewCreator class would be:
public class SinglerCollectionViewCreator : DependencyObject
{
public object GetItemsSource(... // use "propa" snippet to fill this in
public void SetItemsSource(....
public static readonly DependencyProperty ItemsSourceProperty = ...
{
PropertyChangedCallback = (obj, e)
{
obj.SetValue(ItemsControl.ItemsSourceProperty,
new SinglerCollectionView(e.NewValue));
}
}
}
The way this work is, whenever your new SinglerCollectionViewCreator.ItemsSource property is set on any object, the value is wrapped inside your SinglerCollectionView class and the ItemsControl.ItemsSource is set on the same object.
Is there a way to do this entirely in XAML, without writing code?
<ItemsControl Height="100" ItemsSource="{erm:Items FieldName}" Grid.Row="1" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="14" VerticalAlignment="Center" FontWeight="Bold" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have the items source bound like above. The Items markup extension returns observable collection of strings and I have no control over the observable collection.