I develop app for Windows Phone 7 with using of Caliburn Micro.
Hear is a code of app's main parts.
Part of MainView:
<Grid x:Name="LayoutRoot" Background="Transparent">
<controls:Panorama>
<controls:PanoramaItem x:Name="SubPanoramaItem"
DataContext="{Binding SubViewModel}">
<StackPanel>
<toolkit:ListPicker ExpansionMode="FullScreenOnly"
ItemsSource="{Binding DataModeList}">
<toolkit:ListPicker.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Tag}" />
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<toolkit:ListPicker.FullModeItemTemplate>
<DataTemplate>
<StackPanel x:Name="item"
Margin="5, 24, 0, 24"
cal:Action.TargetWithoutContext="{Binding ElementName=SubPanoramaItem,
Path=DataContext}"
cal:Message.Attach="[Event Tap] = [Action Tap($dataContext)]"
Orientation="Horizontal">
<TextBlock FontSize="40"
Text="{Binding PopupText}" />
</StackPanel>
</DataTemplate>
</toolkit:ListPicker.FullModeItemTemplate>
</toolkit:ListPicker>
</StackPanel>
</controls:PanoramaItem>
<!-- Some other code -->
</controls:Panorama>
</Grid>
MainViewModel:
public class MainViewModel: Screen
{
public SubViewModel SubViewModel { get; private set; }
public MainViewModel(SubViewModel subViewModel)
{
SubViewModel = subViewModel;
}
// some other code
}
SubViewModel:
public class SearchViewModel : Screen
{
private ObservableCollection<DateModeItem> _dataModeList =
new ObservableCollection<DateModeItem>()
{
new DataItem
{ PopupText = "Item 1" },
new DataItem
{ PopupText = "Item 2" },
new DataItem
{ PopupText = "Item 3" },
new DataItem
{ PopupText = "Item 4" }
};
public ObservableCollection<DateModeItem> DataModeList
{
get
{
return _dataModeList;
}
private set { _dataModeList = value; }
}
public void Tap(object dataContext)
{
var item = dataContext as DataItem;
if (item != null)
{
var r = new Random();
switch (item.PopupText)
{
case "Item 1":
item.Tag = r.Next(5);
break;
case "Item 2":
item.Tag = r.Next(5, 10);
break;
case "Item 3":
item.Tag = r.Next(10, 15);
break;
case "Item 4":
item.Tag = r.Next(15, 20);
break;
}
}
}
}
DataItem:
public class DataItem
{
public string PopupText { get; set; }
public int Tag { get; set; }
}
As you can see I've attached Action to each StackPanel of DataTemplate in ListPicker. When tap occurs on the item in the list then new random tag must be generated. This tag is inserted into ListPicker's textbox.
And this actions behave very strangely. When I tap on 1, 2 and 4 item, nothing happens at all. When I tap 3 item the app throws exception - "No target found for method Tap". And this what happens when I'm using ListPicker from Silverlight Toolkit.
I've also triend RadListPicker from Telerik's RadConrols library. When I've used it, invocation of action method was unpredictable. Sometimes action invokes method correct. Sometimes nothing happen at all. One I can say surely - with tap on the last item it works less often in the correct way.
What is going on? I can't understand.
Additional info:
I've made a cleaning of all unnecessary stuff from my app and left only code that I described above in the previous post.
Now when I'm using ListPicker - nothing happens at all. List doesn't responds on taps. Sometimes app throws "No target found for method Tap" exception. When I'm using RadListPicker - almost always action not invoked and sometimes (very rarely) invoked correctly.
When you are working with ListPicker you need to add some ElementConventions in Order to bind the Actions of your View to your ViewModel.
Adding a binding convention can be done in the AppBootstrapper class. The code could look something like:
protected override void Configure()
{
ConventionManager.AddElementConvention<ListPicker>(ListPicker.ItemsSourceProperty, "SelectedItem", "SelectionChanged").ApplyBinding =
(viewModelType, path, property, element, convention) => {
if (ConventionManager.GetElementConvention(typeof(ItemsControl)).ApplyBinding(viewModelType, path, property, element, convention))
{
ConventionManager.ConfigureSelectedItem(element, ListPicker.SelectedItemProperty, viewModelType, path);
return true;
}
return false;
}; }
Related
ListViews follow the ItemPicker/Selector pattern of UI controls. Generally speaking, these types of controls, regardless of their platform will have a SelectedItem, and an ItemsSource. The basic idea is that there is a list of items in the ItemsSource, and the SelectedItem can be set to one of those items. Some other examples are ComboBoxes (Silverlight/UWP/WPF), and Pickers (Xamarin Forms).
In some cases, these controls are async ready, and in other cases, code needs to be written in order to handle scenarios where the ItemsSource is populated later than the SelectedItem. In our case, most of the time, the BindingContext (which contains the property bound to SelectedItem) will be set before the ItemsSource. So, we need to write code to allow this to function correctly. We have done this for ComboBoxes in Silverlight for example.
In Xamarin Forms, the ListView control is not async ready, i.e. if the ItemsSource is not populated before the SelectedItem is set, the selected item will never be highlighted on the control. This is probably by design and this is OK. The point of this thread is to find a way make the ListView async ready so that the ItemsSource can be populated after the SelectedItem is set.
There should be straight forward work arounds that can be implemented on other platforms to achieve this, but there are a few bugs in the Xamarin Forms list view that make it seemingly impossible to work around the issue. The sample app I have created is shared between WPF and Xamarin Forms in order to show how the ListView behaves differently on each platform. The WPF ListView for example, is async ready. If the ItemsSource is populated after the DataContext is set on a WPF ListView, the SelectedItem will bind to the item in the list.
In Xamarin Forms, I cannot consistently get SelectedItem two way binding on ListView to work. If I select an item in the ListView, it sets the property on my model, but if I set the property on my model, the item that should be selected is not reflected as being selected in the ListView. After the items have been loaded, when I set the property on my model, no SelectedItem is displayed. This is happening on UWP and Android. iOS remains untested.
You can see the sample problem in this Git repo:
https://ChristianFindlay#bitbucket.org/ChristianFindlay/xamarin-forms-scratch.git . Simply run the UWP, or Android sample, and click Async ListView. You can also run the XamarinFormsWPFComparison sample to see how the WPF version behaves differently.
When you run the Xamarin Forms sample, you will see that there is no item selected after the items load. However in the WPF version, it is selected. Note: It's not highlighted blue, but it is slightly grey indicating that it is selected. This is where my problem is, and the reason I can't work around the async issue.
Here is my code (clone repo for absolute latest code):
public class AsyncListViewModel : INotifyPropertyChanged
{
#region Fields
private ItemModel _ItemModel;
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Public Properties
public ItemModel ItemModel
{
get
{
return _ItemModel;
}
set
{
_ItemModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemModel)));
}
}
#endregion
}
public class ItemModel : INotifyPropertyChanged
{
#region Fields
private int _Name;
private string _Description;
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Public Properties
public int Name
{
get
{
return _Name;
}
set
{
_Name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public string Description
{
get
{
return _Description;
}
set
{
_Description = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description)));
}
}
#endregion
#region Public Methods
public override bool Equals(object obj)
{
var itemModel = obj as ItemModel;
if (itemModel == null)
{
return false;
}
var returnValue = Name.Equals(itemModel.Name);
Debug.WriteLine($"An {nameof(ItemModel)} was tested for equality. Equal: {returnValue}");
return returnValue;
}
public override int GetHashCode()
{
Debug.WriteLine($"{nameof(GetHashCode)} was called on an {nameof(ItemModel)}");
return Name;
}
#endregion
}
public class ItemModelProvider : ObservableCollection<ItemModel>
{
#region Events
public event EventHandler ItemsLoaded;
#endregion
#region Constructor
public ItemModelProvider()
{
var timer = new Timer(TimerCallback, null, 3000, 0);
}
#endregion
#region Private Methods
private void TimerCallback(object state)
{
Device.BeginInvokeOnMainThread(() =>
{
Add(new ItemModel { Name = 1, Description = "First" });
Add(new ItemModel { Name = 2, Description = "Second" });
Add(new ItemModel { Name = 3, Description = "Third" });
ItemsLoaded?.Invoke(this, new EventArgs());
});
}
#endregion
}
This is the XAML:
<Grid x:Name="TheGrid">
<Grid.Resources>
<ResourceDictionary>
<local:ItemModelProvider x:Key="items" />
</ResourceDictionary>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<ListView x:Name="TheListView" Margin="4" SelectedItem="{Binding ItemModel, Mode=TwoWay}" ItemsSource="{StaticResource items}" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="#EEEEEE" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Label Text="{Binding Name}" TextColor="#FF0000EE" VerticalOptions="Center" />
<Label Text="{Binding Description}" Grid.Row="1" VerticalOptions="Center" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ActivityIndicator x:Name="TheActivityIndicator" IsRunning="True" IsVisible="True" Margin="100" />
<StackLayout Grid.Row="1" Orientation="Horizontal">
<Label Text="Name: " />
<Label Text="{Binding ItemModel.Name}" />
<Label Text="Description: " />
<Label Text="{Binding ItemModel.Description}" />
<Button Text="New Model" x:Name="NewModelButton" />
<Button Text="Set To 2" x:Name="SetToTwoButton" />
</StackLayout>
</Grid>
Code Behind:
public partial class AsyncListViewPage : ContentPage
{
ItemModelProvider items;
ItemModel two;
private AsyncListViewModel CurrentAsyncListViewModel => BindingContext as AsyncListViewModel;
public AsyncListViewPage()
{
InitializeComponent();
CreateNewModel();
items = (ItemModelProvider)TheGrid.Resources["items"];
items.ItemsLoaded += Items_ItemsLoaded;
NewModelButton.Clicked += NewModelButton_Clicked;
SetToTwoButton.Clicked += SetToTwoButton_Clicked;
}
private void SetToTwoButton_Clicked(object sender, System.EventArgs e)
{
if (two == null)
{
DisplayAlert("Wait for the items to load", "Wait for the items to load", "OK");
return;
}
CurrentAsyncListViewModel.ItemModel = two;
}
private void NewModelButton_Clicked(object sender, System.EventArgs e)
{
CreateNewModel();
}
private void CreateNewModel()
{
//Note: if you replace the line below with this, the behaviour works:
//BindingContext = new AsyncListViewModel { ItemModel = two };
BindingContext = new AsyncListViewModel { ItemModel = GetNewTwo() };
}
private static ItemModel GetNewTwo()
{
return new ItemModel { Name = 2, Description = "Second" };
}
private void Items_ItemsLoaded(object sender, System.EventArgs e)
{
TheActivityIndicator.IsRunning = false;
TheActivityIndicator.IsVisible = false;
two = items[1];
}
}
Note: if I change the method CreateNewModel to this:
private void CreateNewModel()
{
BindingContext = new AsyncListViewModel { ItemModel = two };
}
the SelectedItem is reflected on screen. This seems to indicate that the ListView is comparing items based on object reference as opposed to using the Equals method on the objects. I tend to think of this as a bug. But, this is not the only issue here, because if this were the only issue, then clicking the SetToTwoButton should yield the same result.
It is now clear that there are several bugs around this is Xamarin Forms. I have documented the repro steps here:
https://bugzilla.xamarin.com/show_bug.cgi?id=58451
The AdaptListView is a suitable alternative to the ListView control and isn't subject to these issues.
The Xamarin Forms team created a pull request to solve some of the issues here:
https://github.com/xamarin/Xamarin.Forms/pull/1152
But, I don't believe this pull request was ever accepted in to the master branch of Xamarin Forms.
I am using Caliburn.Micro to try to bind items in a ListBox to one of two views but a single viewmodel. I am able to display the items in the ListBox, but when any item is selected I get 'Cannot find view for CustomerViewModel.'
Here are the relevant pieces of this application:
AppBootstrapper:
public class AppBootstrapper : BootstrapperBase
{
public AppBootstrapper()
: base()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
base.DisplayRootViewFor<CustomerWorkspaceViewModel>();
}
}
In my Views/Customers folder I have a CustomerViewModel:
public class CustomerViewModel : Screen
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
NotifyOfPropertyChange(() => Name);
}
}
}
and a CustomerWorkspaceViewModel:
public class CustomerWorkspaceViewModel : DocumentWorkspace<CustomerViewModel>
{
private CustomerViewModel selectedItem;
public CustomerViewModel SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
NotifyOfPropertyChange(() => SelectedItem);
}
}
public CustomerWorkspaceViewModel()
{
Items.AddRange(
new ObservableCollection<CustomerViewModel>
{
new CustomerViewModel {Name = "Customer 1" },
new CustomerViewModel {Name = "Customer 2" },
new CustomerViewModel {Name = "Customer 3" }
});
}
}
I have four views in my Views/Customers folder:
In a Views/Customers/CustomerWorkspace, I have and Edit View and an Read view:
Edit.xaml:
<Grid>
<StackPanel>
<Label Content="Edit View"/>
<TextBlock Foreground="White"
FontSize="20"
Text="{Binding Name}" />
</StackPanel>
</Grid>
and Read.xaml:
<Grid>
<TextBlock Foreground="White"
FontSize="20"
Text="{Binding Name}" />
<Label Content="Read view"/>
</Grid>
Finally I have an empty CustomerView user control in Views/Customers, and a CustomerWorkspaceView in Views/Customers:
<Grid>
<ListBox x:Name="Items"
Margin="5"
SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentControl cal:View.Context="{Binding State, Mode=TwoWay}" cal:View.Model="{Binding SelectedItem}"/>
</Grid>
Finally I have DocumentWorkspace, at the root folder, with AppBootstrapper:
public abstract class DocumentWorkspace<TDocument> : Conductor<TDocument>.Collection.OneActive
where TDocument : class, INotifyPropertyChanged, IDeactivate, IHaveDisplayName
{
public enum DocumentWorkspaceState
{
Read,
Edit
}
DocumentWorkspaceState state = DocumentWorkspaceState.Read;
public DocumentWorkspaceState State
{
get { return state; }
set
{
if (state == value)
return;
state = value;
NotifyOfPropertyChange(() => State);
}
}
}
What I am desiring (and expecting) is when selecting an item in the ListBox, which is composed of DocumentWorkspace objects, which are Conductors, to switch from one view (Edit) to another (Read). The select is working, the SelectedItem setter is getting fired, and the State in DocumentWorkspace is set correctly. But Caliburn.Micro cannot seem to find the view for the resulting CustomerViewModel that is SelectedItem. I've really tried to include in this post only what is needed to reproduce the problem here.
Note the documentation for what I am trying to do follows the discussion at
So mvermef pointed me in the right direction. I needed to fix my namespaces. I also needed to create a folder called "Customers" under "Views" and move the Edit and Read views to it. Then I needed to fix my namespaces again. Resharper is a great tool for that, by the way.
So what was wrong, besides the namespaces? In the CustomerWorkspaceView, I had
<ContentControl cal:View.Context="{Binding State, Mode=TwoWay}" cal:View.Model="{Binding SelectedItem}"/>
This caused Caliburn.Micro to look for a State property in the CustomerWorkspaceViewModel (actually in the base class DocumentWorkspace) and in this case the result was "Read". It just needs a string here. There was a "Read" View, but it was in a folder called "CustomerWorkspace". It needed to be in a folder called Customer, as the SelectedItem property is of type CustomerViewModel.
It turns out I didn't even need the empty CustomerView. As long as Caliburn.Micro can find a folder with the same name as your ViewModel type minus "ViewModel" and the namespaces match the folder structure, it should find your view.
Here's my complete folder structure:
Im trying to supply a data template to my ribbon.
The ribbon is declared as following, and has an ItemTemplate attached to it.
<r:Ribbon Name="RibbonMain"
ItemTemplate="{StaticResource HomeRibbonTabTemplate}">
</r:Ribbon>
The Datatemplate is the following:
<Window.Resources>
<DataTemplate DataType="{x:Type local:RibbonContainer}"
x:Key="HomeRibbonTabTemplate">
<r:RibbonTab Header="{Binding Path=HeaderName}">
<r:RibbonGroup Header="{Binding Path=GroupName}">
</r:RibbonGroup>
</r:RibbonTab>
</DataTemplate>
</Window.Resources>
I do then attach the ItemsSource:
public MainWindow()
{
InitializeComponent();
var RibbonTabData = new ObservableCollection<RibbonContainer>();
RibbonTabData.Add(new RibbonContainer("HeaderName", "GroupName"));
RibbonMain.ItemsSource = RibbonTabData;
}
Lastly the class: (Which just contains two string fields)
class RibbonContainer
{
public string HeaderName
{
get;
set;
}
public string GroupName
{
get;
set;
}
public RibbonContainer(string _headername, string _groupname)
{
HeaderName = _headername;
GroupName = _groupname;
}
}
I get the unimpressive result of showing the fully qualified class name in the tab header and neither is the ribbongroup showing. (This is what the datatemplate should solve?)
What to do?
Best regards
I'm not exactly sure where to start, but perhaps with a short warning that, in trying to create a RibbonControl totally from data Binding and data items, you really are opening up a huge can of whoop ass on yourself. This is because the developers that designed the code for it used unconventional patterns for some of it and failed to adequately document how to do things with it. Some of the best sources will be found by searching on this website.
So anyway, if you're up for a painful, uphill struggle, read on. Your first mistake was trying to use a DataTemplate for the RibbonTab because it is extends System.Windows.Controls.ItemsControl and therefore requires a HierarchicalDataTemplate. Your second mistake was declaring the RibbonTab inside the template, as #devhedgehog mentioned in a comment.
You third mistake was setting the x:Key value for your DataTemplate and applying it to the Ribbon.ItemsTemplate property... I know, I know... a sensible enough thing to do if this wasn't a RibbonControl. You'll have to ask those developers as to why that doesn't work, but you're better off just accepting that it doesn't and adapting your code. You just need to remove the x:Key value and the Ribbon.ItemsTemplate property and let the Framework apply the template implicitly.
Now if you ever want more than one RibbonGroup, then your fourth mistake was defining that in the template for the RibbonTab. If you're going to do this properly, then your data classes will need to match the various levels of UI elements in the Ribbon. By this, I mean that you need to create a RibbonGroupData class too. That class needs a collection of RibbonButtonData objects that supply the data to each RibbonButton in the UI. So you should end up with something like this:
public class RibbonTabData : BaseDataType
{
private string name = string.Empty;
private ObservableCollection<RibbonGroupData> ribbonGroupData = new ObservableCollection<RibbonGroupData>();
public string Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
public ObservableCollection<RibbonGroupData> RibbonGroupData
{
get { return ribbonGroupData; }
set { ribbonGroupData = value; NotifyPropertyChanged("RibbonGroupData"); }
}
}
public class RibbonGroupData : BaseDataType
{
private string name = string.Empty;
private ObservableCollection<RibbonButtonData> ribbonButtonData = new ObservableCollection<RibbonButtonData>();
public string Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
public ObservableCollection<RibbonButtonData> RibbonButtonData
{
get { return ribbonButtonData; }
set { ribbonButtonData = value; NotifyPropertyChanged("RibbonButtonData"); }
}
}
public class RibbonButtonData : BaseDataType
{
private string name = string.Empty;
public string Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
}
The BaseDataType class just implements the INotifyPropertyChanged interface. Of course, you'd need to add extra properties for ICommands and image sources, etc. You might even need different RibbonButtonData classes with different properties for different types of RibbonButtons and then you'd need a common RibbonButtonBaseData class that they all extended, so your collection could contain all the different types together. So there's lots more for you to do, but given this example code, you could display it in the Ribbon like this:
<Ribbon:RibbonWindow.Resources>
<HierarchicalDataTemplate DataType="{x:Type DataTypes:RibbonTabData}"
ItemsSource="{Binding RibbonGroupData}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type DataTypes:RibbonButtonData}">
<Ribbon:RibbonButton Label="{Binding Name}"
LargeImageSource="/WpfRibbonApplication1;component/Images/LargeIcon.png" />
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type DataTypes:RibbonGroupData}"
ItemsSource="{Binding RibbonButtonData}">
<Ribbon:RibbonGroup Header="{Binding Name}" />
</HierarchicalDataTemplate>
</Ribbon:RibbonWindow.Resources>
<Ribbon:Ribbon x:Name="Ribbon" ItemsSource="{Binding RibbonTabData}" />
Now in the view model that is set as the DataContext for the Window, I can add some dummy data to test that it all works:
RibbonTabData.Add(new RibbonTabData() { Name = "Tab 1", RibbonGroupData = new ObservableCollection<RibbonGroupData>() { new RibbonGroupData() { Name = "Group 1", RibbonButtonData = new ObservableCollection<RibbonButtonData>() { new RibbonButtonData() { Name = "Button 1" }, new RibbonButtonData() { Name = "Button 2" }, new RibbonButtonData() { Name = "Button 3" } } }, new RibbonGroupData() { Name = "Group 2", RibbonButtonData = new ObservableCollection<RibbonButtonData>() { new RibbonButtonData() { Name = "Button 1" }, new RibbonButtonData() { Name = "Button 2" } } } } });
RibbonTabData.Add(new RibbonTabData() { Name = "Tab 2" });
RibbonTabData.Add(new RibbonTabData() { Name = "Tab 3" });
And we get this:
However, even with this helpful start, you've still got a lot more work to do.
By reading Sheridans answer I managed to create the following result:
(Different controls with the possibility to attach event handlers to wanted control).
How I did the event handling (example with ribbonbutton)
Attach a tag property to your ribbonbutton template (with databinding of course)
Attach an loaded event into your ribbonbutton template
Create a dictionary: (in your windowname.xaml.cs
public Dictionary<string, List<RoutedEventHandler>> EventLibrary = new Dictionary<string, List<RoutedEventHandler>>();
Add an event to dictionary and extend the string with type of event
EventLibrary.Add("NAME_RIBBONBUTTON_CLICKEVENT", new List<RoutedEventHandler> { new RoutedEventHandler(RibbonButton_Test)});
This is the event loaded code:
private void RibbonButton_Loaded(object sender, EventArgs e)
{
System.Windows.Controls.Ribbon.RibbonButton cmd = (System.Windows.Controls.Ribbon.RibbonButton)sender;
if (EventLibrary.ContainsKey(cmd.Tag.ToString() + "_CLICKEVENT"))
{
List<RoutedEventHandler> value = EventLibrary[cmd.Tag.ToString() + "_CLICKEVENT"];
for (int i = 0; i < value.Count; i++)
{
cmd.AddHandler(RibbonButton.ClickEvent, value[i]);
}
}
}
Here is a link to old post in WPF blog, there you can download archive with solution, where you can find couple of useful things:
ViewModels for all Ribbon controls
Styles with all appropriate bindings
So at the end using above things I receive more simple solution:
<HierarchicalDataTemplate DataType="{x:Type ribbonVM:RibbonTabVM}" ItemsSource="{Binding Groups}">
<RibbonTab DataContext="{Binding}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type ribbonVM:RibbonGroupVM}" ItemsSource="{Binding Controls}">
<RibbonGroup DataContext="{Binding}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type ribbonVM:RibbonButtonVM}">
<RibbonButton DataContext="{Binding}" />
</DataTemplate>
The only thing I added to VMs were collections of child elements.
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
I am trying to use an attached behavior to execute a command in my ViewModel when the user Double Clicks on the list item.
I have reviewed a number of articles on the subject, and tried creating a simple test application but am still having problems eg.
Firing a double click event from a WPF ListView item using MVVM
My simple test ViewModel has 2 collections, one that returns a list of strings and the other that returns a List of ListViewItem types
public class ViewModel
{
public ViewModel()
{
Stuff = new ObservableCollection<ListViewItem>
{
new ListViewItem { Content = "item 1" },
new ListViewItem { Content = "item 2" }
};
StringStuff = new ObservableCollection<string> { "item 1", "item 2" };
}
public ObservableCollection<ListViewItem> Stuff { get; set; }
public ObservableCollection<string> StringStuff { get; set; }
public ICommand Foo
{
get
{
return new DelegateCommand(this.DoSomeAction);
}
}
private void DoSomeAction()
{
MessageBox.Show("Command Triggered");
}
}
Here is the attached property which is like may other examples you see:
public class ClickBehavior
{
public static DependencyProperty DoubleClickCommandProperty = DependencyProperty.RegisterAttached("DoubleClick",
typeof(ICommand),
typeof(ClickBehavior),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ClickBehavior.DoubleClickChanged)));
public static void SetDoubleClick(DependencyObject target, ICommand value)
{
target.SetValue(ClickBehavior.DoubleClickCommandProperty, value);
}
public static ICommand GetDoubleClick(DependencyObject target)
{
return (ICommand)target.GetValue(DoubleClickCommandProperty);
}
private static void DoubleClickChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
ListViewItem element = target as ListViewItem;
if (element != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
element.MouseDoubleClick += element_MouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.MouseDoubleClick -= element_MouseDoubleClick;
}
}
}
static void element_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(ClickBehavior.DoubleClickCommandProperty);
command.Execute(null);
}
}
In my main window, I have defined the style which sets the attached behaviour and binds to the Foo command
<Window.Resources>
<Style x:Key="listViewItemStyle" TargetType="{x:Type ListViewItem}">
<Setter Property="local:ClickBehavior.DoubleClick" Value="{Binding Foo}"/>
</Style>
</Window.Resources>
Works fine when ListViewItems are defined:
<!-- Works -->
<Label Grid.Row="2" Content="DoubleClick click behaviour:"/>
<ListView Grid.Row="2" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}">
<ListViewItem Content="Item 3" />
<ListViewItem Content="Item 4" />
</ListView>
This works too, when bound to the list of type ListViewItem:
<!-- Works when items bound are of type ListViewItem -->
<Label Grid.Row="3" Content="DoubleClick when bound to ListViewItem:"/>
<ListView Grid.Row="3" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}" ItemsSource="{Binding Stuff}">
</ListView>
But this doesn't:
<!-- Does not work when items bound are not ListViewItem -->
<Label Grid.Row="4" Content="DoubleClick when bound to string list:"/>
<ListView Grid.Row="4" Grid.Column="1" ItemContainerStyle="{StaticResource listViewItemStyle}" ItemsSource="{Binding StringStuff}">
</ListView>
In the output window you see the error, but finding it difficult to understand what is wrong.
System.Windows.Data Error: 39 : BindingExpression path error: 'Foo' property not found on 'object' ''String' (HashCode=785742638)'. BindingExpression:Path=Foo; DataItem='String' (HashCode=785742638); target element is 'ListViewItem' (Name=''); target property is 'DoubleClick' (type 'ICommand')
So my quesion is: How can you get the Command wired up correctly to each ListViewItem when you bind your ListView to a list of Model objects?
Thanks.
The problem is that the DataContext for the Binding is the string. Since there is no Foo property of the string class, you are getting an error. This doesn't happen in the other cases because they inherit their DataContext from the parent (this doesn't happen for automatically generated containers for data items - their DataContext is the data item).
If you change your binding to use the parent ListView's DataContext, it should work fine:
Value="{Binding DataContext.Foo, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"