I am having an absolute headache figuring this out. I badly need some help with this.
I have a listbox populated with items called with a public static void RSS feed class. Once the listbox populates with the databound items, I click on an item and it passes it through to my pivot page. However, when I flick left or right, all I get is the same image. That is my problem, and what I would like to have happen is if the user flicks left, it loads the previous RSS image. I would like it to also go to the next picture if the If the user scrolls right.
The community has been helpful in providing links to some things, or saying to not use the listbox, etc. However while I am new to all of this, I would just like concrete help with the code i have to achieve what I have in mind. It's nothing personal -- I just need to take babysteps with this before I get worked up with other things I have no clue about.
Here is all my relevant code.
Page 1 Xaml:
<ListBox x:Name="listbox" HorizontalContentAlignment="Stretch" ItemsSource="{Binding items}" SelectionChanged="listbox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Stretch="Fill" Height="60" Width="85" Source="{Binding Url}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Page1 C# Code Behind:
namespace Imaged
{
public partial class UserSubmitted : PhoneApplicationPage
{
private const string Myrssfeed = "http://feeds.bbci.co.uk/news/rss.xml";
public UserSubmitted()
{
InitializeComponent();
//This next function calls the RSS service, and returns the (items) and binds it to
//{listbox.ItemsSource = items;}. I am unable to reference the count of the items, or
//the array of it for some reason? The images load once the page loads.
RssService.GetRssItems(Myrssfeed, (items) => { listbox.ItemsSource = items; }, (exception) => { MessageBox.Show(exception.Message); }, null);
}
}
}
Once the listbox fills I am now trying to pass the selection by the user to a pivot page. I want that same image to show up in the pivot, and when the user pivots left or right, it shows the previous image or next image in the collection.
The Pivot Page I am trying to pass this to, XAML:
<Grid x:Name="LayoutRoot" Background="Transparent">
<!--Pivot Control-->
<controls:Pivot Title="{Binding Title}">
<!--Pivot item one-->
<controls:PivotItem x:Name="item1">
<Image Source="{Binding Url}"/> <!--I take it this is causing the pics to be the same?-->
</controls:PivotItem>
<!--Pivot item two-->
<controls:PivotItem x:Name="item2">
<Image Source="{Binding Url}"/>
</controls:PivotItem>
<!--Pivot item three-->
<controls:PivotItem x:Name="item3">
<Image Source="{Binding Url}"/>
</controls:PivotItem>
</controls:Pivot>
</Grid>
The RSS Service Class being called:
namespace WindowsPhone.Helpers
{
public class RssService
{
public static void GetRssItems(string rssFeed, Action<IList<RssItem>> onGetRssItemsCompleted = null, Action<Exception> onError = null, Action onFinally = null)
{
WebClient webClient = new WebClient();
// register on download complete event
webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
{
try
{
// convert rss result to model
IList<RssItem> rssItems = new List<RssItem>();
Stream stream = e.Result;
XmlReader response = XmlReader.Create(stream);
{
SyndicationFeed feeds = SyndicationFeed.Load(response);
foreach (SyndicationItem f in feeds.Items)
{
RssItem rssItem = new RssItem(f.Title.Text, f.Summary.Text, f.PublishDate.ToString(), f.Links[0].Uri.AbsoluteUri);
rssItems.Add(rssItem);
}
}
// notify completed callback
if (onGetRssItemsCompleted != null)
{
onGetRssItemsCompleted(rssItems);
}
}
finally
{
// notify finally callback
if (onFinally != null)
{
onFinally();
}
}
};
webClient.OpenReadAsync(new Uri(rssFeed));
}
}
}
and finally the RSSItem Class:
namespace WindowsPhone.Helpers
{
public class RssItem
{
public RssItem(string title, string summary, string publishedDate, string url)
{
Title = title;
Summary = summary;
PublishedDate = publishedDate;
Url = url;
// Get plain text from html
PlainSummary = HttpUtility.HtmlDecode(Regex.Replace(summary, "<[^>]+?>", ""));
}
public string Title { get; set; }
public string Summary { get; set; }
public string PublishedDate { get; set; }
public string Url { get; set; }
public string PlainSummary { get; set; }
}
}
Disclaimer: I don't think that binding this many items to a Pivot control is necessarily the right thing to do. Your mileage may vary, but I think a more virtualized solution would be more efficient. For my tests, it seemed to perform OK, but my little voice tells me that there be dragons here...
I recreated your project to the best of my ability and made some enhancements to get it to do what you wanted. Basically, the trick was using a ViewModel that was shared between both the main list page (UserSubmitted.xaml) and the page with the Pivot items on it (PivotPage1.xaml). By setting both page's DataContext property to the same object, we were able to bind both lists to the same source, thus eliminating the need to pass anything around.
In App.xaml.cs:
public static ViewData ViewModel { get; private set; }
private void Application_Launching(object sender, LaunchingEventArgs e)
{
// note: you should properly Tombstone this data to prevent unnecessary network access
ViewModel = new ViewData();
}
Here is how ViewData is defined:
public class ViewData : INotifyPropertyChanged
{
private string _FeedTitle;
private RssItem _SelectedItem = null;
private ObservableCollection<RssItem> _feedItems = new ObservableCollection<RssItem>();
private const string MyRssfeed = "http://feeds.bbci.co.uk/news/rss.xml";
public ViewData()
{
RssService.GetRssItems(
MyRssfeed,
(title, items) =>
{
App.Current.RootVisual.Dispatcher.BeginInvoke(() =>
{
FeedTitle = title;
FeedItems = new ObservableCollection<RssItem>(items);
});
},
(exception) =>
{
MessageBox.Show(exception.Message);
},
null);
}
public ObservableCollection<RssItem> FeedItems
{
get { return _feedItems; }
set
{
if (_feedItems == value)
return;
_feedItems = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("FeedItems"));
}
}
public string FeedTitle
{
get { return _FeedTitle; }
set
{
if (_FeedTitle == value)
return;
_FeedTitle = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("FeedTitle"));
}
}
public RssItem SelectedItem
{
get { return _SelectedItem; }
set
{
if (_SelectedItem == value)
return;
_SelectedItem = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (PropertyChanged != null)
PropertyChanged(sender, args);
}
}
Once this is established, it's relatively easy to wire up both page's data context properties to App.ViewModel.
Last item was the scrolling and positioning of the selected item when navigating. When you select an item from the list page, the SelectedItem property of the shared ViewModel is bound to the SelectedItem property on the ListBox. After navigation to the details page, we have to find the selected item in the pivot and make it visible:
public PivotPage1()
{
InitializeComponent();
Loaded += (sender, e) =>
{
this.DataContext = App.ViewModel;
var selectedItem = App.ViewModel.SelectedItem;
var pi = ItemPivot.Items.First(p => p == selectedItem);
ItemPivot.SelectedItem = pi;
};
}
Setting the SelectedItem property of the Pivot control scrolls the pivot to the proper item and makes it visible.
The full sample is posted at http://chriskoenig.net/upload/imaged.zip if you want to see it in action.
If I got you correctly, you need to bind listbox in following way:
<ListBox ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}" />
And then bind Pivot in same way:
<Pivot ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}" />
Try the following for the pivot (based on Alex's code)
<Pivot ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}">
<Pivot.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Url}"/>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
It assumes on the pivot page DataContext there is the same object "items" providing access to all the feeditems, and a property SelectedFeed which (as Alex mentioned) supports INotifyPropertyChanged
Related
In my MainView there is a ContentControl which is bound to a CurrentView object. The CurrentView is changed via buttons on the MainView bound to commands.
MainView
<Window>(...)
<RadioButton Content="View1"
Command="{Binding View1Command}"/>
<RadioButton Content="View2"
Command="{Binding View2Command}"/>
<ContentControl Content="{Binding CurrentView}"/>
</Window>
MainVM
(The ObservableObject class implements INotifyPropertyChanged and the RelayCommand class ICommand.)
class MainViewModel : ObservableObject
{
public RelayCommand ViewCommand1 { get; set; }
public RelayCommand ViewCommand2 { get; set; }
public ViewModel2 VM1 { get; set; }
public ViewModel2 VM2 { get; set; }
object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
VM1 = new ViewModel1();
VM1.ContentChanged += (s, e) => OnPropertyChanged();
ViewCommand1 = new RelayCommand(o =>
{
CurrentView = VM1;
});
VM2 = new ViewModel2();
ViewCommand2 = new RelayCommand(o =>
{
CurrentView = VM2;
});
}
}
Those (sub) VM are bound to UserControls which contain image controls and a button to load the image sources from files.
View1
<UserControl x:Class="Project.Views.View1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Project.ViewModels"
d:DataContext="{d:DesignInstance Type=viewModels:ViewModel1}"
mc:Ignorable="d" >
[...]
<Button Command="{Binding LoadImagesCommand}"/>
[...]
<Image Source="{Binding Images[0]}" "/>
<Image Source="{Binding Images[1]}" "/>
[...]
</UserControl>
VM1
class RiJustageViewModel: ObservableObject
{
public event EventHandler ContentChanged;
void OnContentChanged()
{
ContentChanged?.Invoke(this, new EventArgs());
}
public RelayCommand LoadImagesCommand { get; set; }
public ViewModel1()
{
Images = new BitmapImage[9];
LoadImagesCommand = new RelayCommand(o => LoadImages());
}
BitmapImage[] _images;
public BitmapImage[] Images
{
get { return _images; }
set
{
_images = value;
OnContentChanged();
}
}
public void LoadImages()
{
[...]
for (int i = 0; i < files.Length; i++)
{
Images[i] = Utility.BmImageFromFile(files[i]);
}
[...]
}
}
The issue now is that the images are not shown right away after they are loaded. Only after I change the content of the ContentControl to another view and then back to View1 the images are shown.
Is there a way to trigger that display right after the loading is complete without changing the content of the ContentControl?
EDIT:This should be done everytime the user wants to load new images via the button, not only during initialization.
EDIT:
With lidqy's and EldHasp's comments I was able to clean up the VM and the View using ObservableCollection and ItemsControl.
VM
public class ImageItem
{
public string FileName{ get; set; }
public ImageSource Image { get; set; }
public ImageItem(string f, ImageSource im)
{
FileName = f;
Image = im;
}
}
public ObservableCollection<ImageItem> ImageItems { get; set; }
[...]
public void LoadImages()
{
[...]
ImageItems.Clear();
foreach (var file in files)
{
var im = Utility.BmImageFromFile(file);
var f = Path.GetFileName(file);
ImageItems.Add(new ImageItem(f, im));
}
}
View
<ItemsControl ItemsSource="{Binding ImageItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" Rows="3"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="18" />
<RowDefinition Height="200" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding FileName}" Style="{StaticResource ImageDescr}" />
<Image Grid.Row="1" Source="{Binding Image}" Style="{StaticResource ImageTheme}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Very neat.
The ContentChanged event is useless.
Declare the Images property like this:
private ImageSource[] images;
public ImageSource[] Images
{
get { return images; }
set
{
images = value;
OnPropertyChanged();
}
}
In LoadImages(), just assign a new array:
public void LoadImages()
{
...
Images = files
.Select(f => Utility.BmImageFromFile(f))
.ToArray();
}
class RiJustageViewModel: ObservableObject
{
public event EventHandler ContentChanged;
void OnContentChanged()
{
ContentChanged?.Invoke(this, new EventArgs());
}
public RelayCommand LoadImagesCommand { get; set; }
public ViewModel1()
{
// If this is not an observable collection,
// then it makes no sense to create it in advance.
// Images = new BitmapImage[9];
LoadImagesCommand = new RelayCommand(o => LoadImages());
}
// Since it is not an observable collection,
// the more general interface can be used: IEnumerable or IEnumerable <T>.
IEnumerable<BitmapImage> _images;
public IEnumerable<BitmapImage> Images
{
get { return _images; }
set
{
_images = value;
OnContentChanged();
}
}
public void LoadImages()
{
[...]
// For an unobservable collection,
// changing elements does not automatically change their presentation.
// We need to create a NEW collection with
// new elements and assign it to the property.
BitmapImage[] localImages = new BitmapImage[files.Length];
for (int i = 0; i < files.Length; i++)
{
localImages[i] = Utility.BmImageFromFile(files[i]);
}
Images = localImages;
[...]
}
}
This implementation has a drawback - it creates a new collection of images each time.
From memory, it doesn't matter (compared to other WPF costs).
But replacing a collection results in a re-creation of the UI elements that represent it.
And this is already significantly longer delays.
For your task, this is also not important, since this happens very rarely.
But for more loaded scenarios, it is better to use an observable collection (INotifyCollectionChanged) or a bindable list (IBindingList).
It is typical for WPF to use ObservableCollection<T>.
But in the case of asynchronous work with them, you need to take measures to work with it was thread-safe.
For the implementation I have shown, thread safety is not needed.
This is already implemented in the Binding mechanism itself to work with the INotifyPropertyChanged interface.
If you want to display all the images of the _images collection of the current view model (which is the "current view") I would display them in a ListBox and put the image tag and the binding into the ListBox's ItemTemplate.
Also as previously mentioned by others, using an ObservableCollecztion<ImageSource> is strongly recommended since your collection data changes and you want your UI to notice it.
If you don't use an ObservableCollection, the view and images get only updated if the view model as a whole changes.
I want to achieve one-way binding from an ObservableCollection of "struct-like" items to a TextBox that has a TextChanged event. The idea is that as the Comments field of Item accumulates in the TextBox, the TextBox scroll down automatically so that the last line is always in view. The collection is bound to a ListView but I want it bound read-only to the TextBox. I would prefer not to add another method in ResultViewModel but do it in XAML. How would I go about in doing this? TIA
// ViewModel
public class Item
{
public string First { get; set; }
public string Last { get; set; }
public string Comments { get; set; }
}
public class ResultViewModel
{
private ObservableCollection<Item> items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items { get { return items; } }
// member functions
}
public ResultViewModel ViewModel { get; set; }
// What I have
<ListView x:Name="myListView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Items}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Item">
<StackPanel>
<TextBox Text="{x:Bind First}"/>
<TextBlock Text="{x:Bind Last}"/>
<TextBlock Text="{x:Bind Comments}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
// What I want
<TextBox Text="{x:Bind Comments}"/>
I'm afraid you can't do it with XAML alone.
You can create a behavior which will listen to events and add lines to the textbox when the collection is modified.
Dirty example, you will need to include Microsoft.Xaml.Behaviors.Uwp.Managed package:
public class CommentsBehavior : Behavior
{
public ObservableCollection<string> Comments ... // you will need to make it a dependency property
protected virtual void OnAttached()
{
Comments.CollectionChanged += OnCollectionChanged;
}
protected virtual void OnDetaching()
{
Comments.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach(string newItem in e.NewItems)
{
((TextBox)AssociatedObject).Text = ((TextBox)AssociatedObject).Text + '\n' + newItem;
}
}
}
}
And for scrolling - UWP C# Scroll to the bottom of TextBox
But why do you want to use textbox for this? Using list makes more sense.
I am trying to figure out how to display only specific content on a new page and I am wondering how to retrieve that data. For instance I have buttons that are generated from parsed data from an xml sheet and when I click on the button I want the button to direct to a new xaml page I have created and have the data associated with that button displayed on the new xaml page.
First I will link some of the code I am using to store my data from my xml page.
`public int countElements = 0;
public MainViewModel()
{
this.Items = new ObservableCollection<ItemViewModel>();
}
public ObservableCollection<ItemViewModel> Items { get; private set; }
public void LoadData()
{
var elements = from p in unmXdoc.Descendants(dataNamspace + "vevent").Elements(dataNamspace + "properties")
select new ItemViewModel
{
summary = this.GetElementValue(p, "summary"),
description = this.GetElementValue(p, "description"),
categories = this.GetElementValue(p, "dtstamp"),
};
foreach (var element in elements)
{
this.Items.Add(new ItemViewModel()
{
LineOne = element.summary,
LineTwo = element.categories,
LineThree = element.description
});
countElements++;
}
this.IsDataLoaded = true;`
So LineOne is the name of my button and when I click on the button I want LineTwo and LineThree to be loaded on my xaml page that I named LineThreePage.xaml. I will link the xaml code where the buttons are being generated now.
<Grid x:Name="LayoutRoot" Background="Transparent" >
<!--Pivot Control-->
<controls:Pivot Title="" Margin="0,64,0,-63">
<!--Pivot item one-->
<controls:PivotItem>
<!-- Header="Events"-->
<controls:PivotItem.Header>
<TextBlock Text="Events" FontSize="48" ></TextBlock>
</controls:PivotItem.Header>
<!--Double line list with text wrapping-->
<ListBox x:Name="FirstListBox" Margin="0,0,-12,0" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432" Height="78">
<Button Margin="8,0,10,0"
Padding="0,0"
HorizontalContentAlignment="Left"
BorderThickness="0.8"
BorderBrush="Gray"
Background="White"
Width="420"
Click="Button_Click">
<TextBlock Text="{Binding LineOne}"
TextWrapping="Wrap"
Foreground="#8f1020"
Style="{StaticResource PhoneTextNormalStyle}"/>
</Button>
TextWrapping="Wrap" Margin="12,-10,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PivotItem>'
So basically when I click button1 I want to navigate to my lineThreePage.xaml and see the LineTwo and LineThree associated with the LineOne on that page.
Finally I have my button click code below!
private void Button_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri("/lineThreePage.xaml", UriKind.Relative));
}
Essentially, what you are looking for is a way to preserve "state" within an application. There are several ways to do this, a couple of them being App.Current.ApplicationLifetimeObjects and Isolated storage.
One of the first things I do when I setup a new WP project is to sort out a service that I would used to preserve state within the app. Assuming that your FirstListBox in the code above binds to an entity of type "ItemViewModel".
1) Setup a generic Isolated storage class service.... bear in mind that you can adapt this to whatever suites your requirement, I've made a few assumptions such as returning nulls when a value is not found in this code,
public class IsolateStorageStore
{
/// <summary>
/// The iosolated settings store.
/// </summary>
private readonly IsolatedStorageSettings isolatedStorageSettings = IsolatedStorageSettings.ApplicationSettings;
public T ReadValue<T>(string key)
{
return isolatedStorageSettings.Contains(key) ? (T)isolatedStorageSettings[key] : default(T);
}
public void WriteValue<T>(string key, T value)
{
if (isolatedStorageSettings.Contains(key))
{
isolatedStorageSettings[key] = value;
}
else
{
isolatedStorageSettings.Add(key, value);
}
}
}
2) Setup a mechanism for reading/writing a stored value
public static class IsolatedStorageManager
{
private static IsolateStorageStore IsolateStorageStore = new IsolateStorageStore();
public static ItemViewModel FeedItemViewModel
{
get
{
return IsolateStorageStore.ReadValue<ItemViewModel>("ItemFeedsKey");
}
set
{
IsolateStorageStore.WriteValue("ItemFeedsKey", value);
}
}
public static object AnotherItem
{
get
{
return IsolateStorageStore.ReadValue<object>("AnotherItemKey");
}
set
{
IsolateStorageStore.WriteValue("AnotherItemKey", value);
}
}
}
Now that you have effectively a service for reading/writing objects to storage, tweak your code to use it.
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = (sender as Button);
if (button != null)
{
var data = button.DataContext as ItemViewModel;
if (data != null)
{
//Save to isolated storage
IsolatedStorageManager.FeedItemViewModel = data;
//redirect to next Page.
this.NavigationService.Navigate(new Uri("/lineThreePage.xaml", UriKind.Relative));
}
else
{
MessageBox.Show("An error occured, either the sender is not a button or the data context is not of type ItemViewModel");
}
}
else
{
MessageBox.Show("An error occured, either the sender is not a button or the data context is not of type ItemViewModel");
}
}
And finally on your lineThreePage.xaml page, you need to read the values that you have stored in the isolated storage
public lineThreePage()
{
InitializeComponent();
BindData();
}
private void BindData()
{
var data = IsolatedStorageManager.FeedItemViewModel;
if (data != null)
{
//Bind the data to a text box in your xaml named "txtDescription"
txtDescription.Text = data.LineTwo;
}
}
Do a search for Isolated storage on here to find several resources of how to use it.
I have a listview which contains the customer informations. There is a search text box above the that listview. When you type anything into the textbox then it higlights the matched item in the listview. But , the problem is that it makes search only in the visual side of the listview. It doesn't search in the not scrolled side of the listview(buttom of the listview). My code is below. Please have a look.
private void FindListViewItem(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
ListViewItem lv = obj as ListViewItem;
if (lv != null)
{
HighlightText(lv);
}
FindListViewItem(VisualTreeHelper.GetChild(obj as DependencyObject, i));
}
}
private void HighlightText(Object itx)
{
if (itx != null)
{
if (itx is TextBlock)
{
Regex regex = new Regex("(" +TxtSearch.Text + ")", RegexOptions.IgnoreCase);
TextBlock tb = itx as TextBlock;
if (TxtSearch.Text.Length == 0)
{
string str = tb.Text;
tb.Inlines.Clear();
tb.Inlines.Add(str);
return;
}
string[] substrings = regex.Split(tb.Text);
tb.Inlines.Clear();
foreach (var item in substrings)
{
if (regex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Lime;
tb.Inlines.Add(runx);
if (tb.IsMouseOver)
{
tb.IsEnabled = false;
}
}
else
{
tb.Inlines.Add(item);
tb.IsEnabled = false;
}
}
return;
}
else
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(itx as DependencyObject); i++)
{
HighlightText(VisualTreeHelper.GetChild(itx as DependencyObject, i));
}
}
}
}
This happens because the ListView, by default, uses virtualization for its content. This means that the ListViewItems are created when they are needed. If you didn't scroll the ListView, some ListViewItems will not be created and VisualTreeHelper.GetChildrenCount will not be able to return those ListViewItems.
To achieve what you want, you can:
disable ListView virtualization by setting: VirtualizingStackPanel.IsVirtualizing="False" on your ListView (not recommended if you have many items in your list).
you can enforce the creation of the ListViewItem which are not visible by calling IItemContainerGenerator.GenerateNext and IItemContainerGenerator.PrepareItemContainer (not recommended at all). (also take a look at this)
find a better logic to highlight your ListViewItems :) (recommended). (for example search on your collection for the items you want to highlight instead of searching on the UI elements that are only displaying your items. Then mark the items found as highlighted and base on this, display the ListViewItems accordingly (with a different template or style))
You can do this in several ways. Here is one way that i think would work in your scenario and partially with your code and still use virtualization.
Use a data template for the list view item, and create an event handler for loaded event, something like:
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Loaded="FrameworkElement_OnLoaded"/>
</DataTemplate>
</ListView.ItemTemplate>
In the OnLoaded event handler call your HighlightText method on the sender:
HighlightText(sender)
In order to trigger the loaded event you'll need to refresh the list view each time the search string will change. Something like ListView.Items.Refresh() should do it.
You could improve this a bit by adding a small timer on the search text changed, so the user will be able to finish typing when it's searching for something.
There are other, more elegant ways to handle this, but for your case i think this should work.
In Addition to my Comment:
Use a Property and a Observable Collection and directly filter on that Collection.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Entry> MyCollection {get;set;}
public MainWindow()
{
InitializeComponent();
MyCollection = new ObservableCollection<Entry>();
MyCollection.Add(new Entry() { Name = "Test" });
MyCollection.Add(new Entry() { Name = "ABCD" });
MyCollection.Add(new Entry() { Name = "TESTABC" });
MyCollection.Add(new Entry() { Name = "BCDtest" });
this.MyListView.DataContext = this;
}
private void searchTerm_KeyUp(object sender, KeyEventArgs e)
{
String term = ((TextBox)sender).Text;
foreach (Entry entry in this.MyCollection)
{
if (entry.Name.Contains(term))
entry.Highlight();
else
entry.UnHighlight();
}
}
}
public class Entry : INotifyPropertyChanged
{
public String Name { get; set; }
public Color BGColor { get; set; }
public SolidColorBrush BGBrush
{
get
{
return new SolidColorBrush(this.BGColor);
}
}
public Entry()
{
this.UnHighlight();
}
public void Highlight()
{
this.BGColor = Colors.Yellow;
this.NotifyPropertyChanged("BGBrush");
}
public void UnHighlight()
{
this.BGColor = Colors.White;
this.NotifyPropertyChanged("BGBrush");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
along with
<Grid>
<DockPanel>
<TextBox DockPanel.Dock="Top" Name="searchTerm" KeyUp="searchTerm_KeyUp"></TextBox>
<ListView Name="MyListView" ItemsSource="{Binding MyCollection}" >
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding BGBrush}" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</Grid>
And you are done. No need to manually touch the listview at any time. (To Increase Performance: For the Raising of the PropertyChanged Event you may want to add a check, if its really changing, or if it has been set to white from white etc.)
I am using MVVM light in conjunction with EF4 and SQL CE 4, but I am having issues with my observable collection. My application doesn't neccessarily need to use the mvvm pattern, but since I need the benefits of an observablecollection I have decided to learn how to integrate it. I can successfully link my database of property entitites to my listbox and display them, I can also link some properties of these entities to textboxes, but where I am stuck is when I try to update these properties by typing in the textbox. Here is my xaml code for a textbox and the listbox:
<TextBox Text="{Binding SaleTitle, ValidatesOnDataErrors=true, Mode=TwoWay}"
<ListBox Height="424"
Margin="24,80,0,0"
x:Name="listBoxProperties"
VerticalAlignment="Top"
ItemTemplate="{StaticResource propertySummaryTemplate}"
IsSynchronizedWithCurrentItem="True"
Width="216" BorderThickness="0" Background="{x:Null}"
FontFamily="Segoe UI"
ItemsSource="{Binding PropertyList}"
SelectedItem="{Binding CurrentProperty, Mode=TwoWay}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
UseLayoutRounding="True"
HorizontalAlignment="Left"
ScrollViewer.VerticalScrollBarVisibility="Disabled" >
</ListBox>
Here is the code of part of my MainViewModel:
private string _SaleTitle;
public string SaleTitle
{
get
{
if (CurrentProperty != null)
return CurrentProperty.SaleTitle;
else
return "";
}
set
{
_SaleTitle = value;
RaisePropertyChanged("SaleTitle");
}
}
private RelayCommand loadCommand;
public ICommand LoadCommand
{
get
{
if (loadCommand == null)
loadCommand = new RelayCommand(() => Load());
return loadCommand;
}
}
private void Load()
{
PropertyList = new ObservableCollection<Property>((from property in entities.Properties.Include("Images")
select property));
propertyView = CollectionViewSource.GetDefaultView(PropertyList);
if (propertyView != null)
propertyView.CurrentChanged += new System.EventHandler(propertyView_CurrentChanged);
RaisePropertyChanged("CurrentContact");
RaisePropertyChanged("SaleTitle");
RaisePropertyChanged("Address");
RaisePropertyChanged("AuctioneerName");
RaisePropertyChanged("AgentName");
RaisePropertyChanged("Price");
RaisePropertyChanged("NextBid");
RaisePropertyChanged("Status");
}
void propertyView_CurrentChanged(object sender, System.EventArgs e)
{
RaisePropertyChanged("CurrentContact");
RaisePropertyChanged("SaleTitle");
RaisePropertyChanged("Address");
RaisePropertyChanged("AuctioneerName");
RaisePropertyChanged("AgentName");
RaisePropertyChanged("Price");
RaisePropertyChanged("NextBid");
RaisePropertyChanged("Status");
}
private Property _CurrentProperty;
public Property CurrentProperty
{
get
{
if (propertyView != null)
return propertyView.CurrentItem as Property;
return null;
}
set
{
_CurrentProperty = value;
RaisePropertyChanged("CurrentProperty");
}
}
public ObservableCollection<Property> PropertyList
{
get
{
return propertyList;
}
set
{
if (propertyList == value)
{
return;
}
var oldValue = propertyList;
propertyList = value;
// Update bindings, no broadcast
RaisePropertyChanged(PropertiesPropertyName);
}
}
public MainViewModel()
{
if (IsInDesignMode)
{
// Code runs in Blend --> create design time data.
}
else
{
// Code runs "for real"
entities = new Model1Container1();
}
}
////public override void Cleanup()
////{
//// // Clean up if needed
//// base.Cleanup();
////}
}
}
The listbox is populated successfully with the content from current selected item, but when I type in it and click out of it or do anything to lose focus it simply goes back to what was there before.
Take a look at your SaleTitle property definition. It Reads value from CurrentProperty.Saletitle but sets value to local field which is not used anythere.