How are databound views rendered? - windows-phone-7

When a Windows Phone 7 application opens a view, a certain order of business is followed in order to create. As far as constructors and events go, I have found this order to be true:
Constructor
OnNavigatedTo
OnLoaded
However, I am in a position where I need to databind a List to a ListBox after the basic view (background, other elements etc) has loaded. So I need to know when and how to know that the view is loaded before I get on with the data binding.
I have tried to do this on the OnLoaded-event, but it seems like if I do the data binding here - and right after it traverse those elements - they don't seem to exist yet (the VisualTreeHelper-class can't seem to find the nodes). So as you see, I am stuck.
Any help would be greatly appreciated.
Edit: As requested, here is some more information about what's going on.
My List is populated by some custom (not too complicated) objects, including an asynchronously loaded image (courtesy of delay.LowProfileImageLoader) and a rectangle.
The XAML:
<ListBox x:Name="ChannelsListBox" ItemsSource="{Binding AllChannels}">
//...
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="ChannelTile" Margin="6,6,6,6" Tap="ChannelTile_Tap" Opacity="0.4">
<!-- context menu goes here -->
<Rectangle Width="136" Height="136" Fill="{StaticResource LightGrayColor}" />
<Image Width="136" Height="136" delay:LowProfileImageLoader.UriSource="{Binding ImageUri}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The code-behind:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
UpdateApplicationBar();
pickChannelsViewModel = new PickChannelsViewModel();
DataContext = pickChannelsViewModel;
if (hasUpdatedTiles)
{
pickChannelsViewModel.IsLoading = false; // Set by UpdateTiles()
}
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
// This is where I would data bind the list (instead of in XAML)
UpdateTiles(); // Traverses the list and changes opacity of "selected" items.
}
protected void UpdateTiles()
{
foreach (var item in ChannelsListBox.Items)
{
if (pickChannelsViewModel.SelectedChannels.Contains(item as Channel))
{
var index = ChannelsListBox.Items.IndexOf(item);
// This returns null when databinding in codebehind,
// but not in XAML
ListBoxItem currentItem = ChannelsListBox.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem;
if (currentItem != null && VisualTreeHelper.GetChildrenCount(currentItem) == 1)
{
var OuterWrapper = VisualTreeHelper.GetChild(currentItem, 0);
var MiddleWrapper = VisualTreeHelper.GetChild(OuterWrapper, 0);
var InnerWrapper = VisualTreeHelper.GetChild(MiddleWrapper, 0);
Grid currentItemGrid = VisualTreeHelper.GetChild(InnerWrapper, 0) as Grid;
currentItemGrid.Opacity = 1.0;
}
}
}
pickChannelsViewModel.IsLoading = false;
hasUpdatedTiles = true;
}
The items themselves are in-memory (fetched from REST at an earlier stage in the application), so should be available instantaneously.
The issue I am trying to resolve is a fairly long load time on this particularly view (there is about 140 of these items being created, then filtered through and changing the opacity).

I believe you are doing something like:
myListBox.ItemSource=myList;
Once you set the ItemSource of a ListBox the changes in your List should be reflected in the ListBox at all times. If the ListBox is empty the reason must be that the List is not being populated properly or invalid Bindings in the ItemTemplate. You should debug and find out if your List has any items by inserting a breakpoint in the Loaded() method. Also, you've not mentioned what items does your List contains or, where is it being populated in the application? Incomplete information doesn't help anyone.

Related

Can I bind the Height of a XAML Grid back to my C# back end code?

I have some XAML code that looks like this. It names four grids and then in the back end my C# does something based on the values. As I can not have duplicate names I created four names.
But I would like to simplify the back-end code so is it possible that I could bind back the value of the height from my XAML > ViewModel and then check that value in my C#
<Grid IsVisible="{Binding AVisible}" VerticalOptions="FillAndExpand">
<Grid x:Name="aWords" VerticalOptions="FillAndExpand" >
    <Frame VerticalOptions="FillAndExpand">
       <Frame.Content>
          <Grid x:Name="aArea" VerticalOptions="CenterAndExpand">
             <Label Text="{Binding Detail}"
            </Grid>
         </Frame.Content>
      </Frame>
   </Grid>
</Grid>
<Grid IsVisible="{Binding BVisible}" VerticalOptions="FillAndExpand">
<Grid x:Name="bWords" VerticalOptions="FillAndExpand" >
    <Frame VerticalOptions="FillAndExpand">
      <Frame.Content>
          <Grid x:Name="bArea" VerticalOptions="CenterAndExpand">
             <Label Text="{Binding Detail}"
            </Grid>
         </Frame.Content>
      </Frame>
   </Grid>
</Grid>
and in C#
var a = aWords.Height;
var b = aArea.Height;
if (b > a) doSomething();
var c = aWords.Height;
var d = aArea.Height;
if (d > c) doSomething();
What I would like to do is like this:
if (vm.AreaHeight > vm.WordsHeight) doSomething();
The way you can pass those values to your ViewModel is by using the binding context of your page for interacting with the ViewModel methods and properties.
First, create a couple of properties in your ViewModel to hold the Height of your objects: i.e.:
public int AreaHeight { get; set; }
public int WordsHeight { get; set; }
Then, on your page override the event: OnAppearing() in order to get the Height from your grids using the x:Name attribute, then you will call from your ViewModel the method: doSomething() passing the obtained values as parameters. i.e.:
protected override void OnAppearing()
{
base.OnAppearing();
// Get your height from your grids here ...
// int gridAHeight = aWords.Height;
// int gridBHeight = aArea.Height;
(BindingContext as YourViewModel).doSomething(gridAHeight, gridBHeight);
}
Finally, in your ViewModel, implement the method doSomething() and catch the parameters obtained from the view page by assigning them to your previously created properties. i.e:
public void DoSomething(int gridAHeight, int gridBHeight)
{
AreaHeight = gridAHeight;
WordsHeight = gridBHeight;
if (AreaHeight < WordsHeight)
{
// place your logic here...
}
}
That is all you need. It is just basic MVVM pattern.
Passing heights or binding heights to a ViewModel violates MVVM pattern. ViewModels shouldn't know anything about a specific view.
I would suggest refactoring your view and finding a different layout depending on the specifics needed for the page. A possible solution would be to use a ListView with labels and restricting the content in the ObservableCollection so you don't have duplicates.
If you have to have it set up that way, I would suggest using MaxLines and/or LineBreakMode to restrict the height of your Labels.
Issues with doing logic based on height:
FontSize could be different based on AccessibilitySettings specified in the users phone
Rotating the screen will change how much of the label is wrapped due to the label expanding. This would force you to do a recalculation again.
Basically, you can't. There isn't the height in XAML, but rather HeightRequest. The layout is a complex thing and the height may not end up being what is your HeightRequest, it is just a suggested value. So you can only change that suggested value from the view model that is not the problem, but that is not what you asked for.

Xamarin XAML variable scope not working like expected

I have a really odd problem with variable scopes. A Listview named "TodoListView" is defined via xaml, and it's ItemSource populated from a SQListe database. Works. Inside the ListView I have a ViewCell to display the data row-wise.
<ContentPage ... x:Class="JanitorPro.MainPage" ...>
<StackLayout>
<ListView x:Name="TodoListView" Margin="20" ItemSelected="OnListItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="{Binding name}" VerticalTextAlignment="Center" HorizontalOptions="StartAndExpand" />
<Switch HorizontalOptions="End" IsToggled="{Binding done}" Toggled="DoneSwitchToggled"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
The codebehind looks like this (some irrelevant portions removed):
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// load Database
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
}
async void OnReloadButtonClicked(object sender, EventArgs e)
{
Debug.WriteLine("Reload Button Click");
TodoListView.ItemsSource = await App.TodoDatabase.GetItemsAsync("SELECT * FROM [TodoItem]");
Debug.WriteLine("Reload done.");
}
async void OnListItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (e.SelectedItem != null)
{
await Navigation.PushAsync(new TodoItemPage
{
BindingContext = e.SelectedItem as TodoItem
});
}
}
private void DoneSwitchToggled(object sender, ToggledEventArgs e)
{
// TodoItem i = null;
TodoItem i = TodoListView.SelectedItem;
if (i != null)
{
Debug.WriteLine("Toggle: {0}", i.id);
}
}
}
}
The oddity has two stages. Before I inserted the DoneSwitchToggled event handler, every occurrance of TodoListView.ItemsSource got a red underline under TodoListView and a hint that "The name TodoListView does not exist in the current context". OK, I thought that VS was not smart enough to find a definition in the xaml file, because, despite of the warning, the program compiled and ran fine. TodoListView gets initialized and does correctly display the rows of the underlying database, so it does clearly exist at runtime.
Things went wild when I added the DoneSwitchToggled event handler to both XAML and the codebehind. All the sudden the program won't compile any longer but bail out with a CS0103 error "The name "TodoListView" does not exist in the current context". The error appears three times, with the line numbers pointing to the other occurrances of TodoListView in onAppearing() and OnReloadButtonClicked(). Huh? How the heck can the addition of a variable reference in an event handler render that variable invalid in completely different methods? OK, there was something fishy with the variable before (still don't know what ...), but it worked. Now it doesn't any more, whch doesn't make any sense for me. Furthermore, if I comment out the offending line in the DoneSwitchToggled event handler, and insert a dummy definition for i, like so:
TodoItem i = null;
// TodoItem i = TodoListView.SelectedItem;
everything is like before, VS still underlines the other appearances of TodoListView, but now the program builds and runs ok again.
Anyone who can explain this effect, and show me how correct my code? I think the objective is clear: DoneSwitchToggled is supposed to write back the switch value into the database (and do some other processing not shown in my stripped down sample), and though the "sender" object is correctly set to reference my button, I found no way to access the underlying data binding, since ToggledEventArgs unfortunately does seem to only pass the switch position "true" or "false", but - unlike the OnListItemSelected event handler - not pass any reference to the bound row through the second argument. So my idea was to use ListView.SelectedItem for this purpose.
Finally I figured it out myself. This seems to be a glitch in VS 2017. There is nothing wrong with TodoListView, so error CS0103 is misleading nonsense.
What VS really means is an error CS0266. TodoListView is defined by a generic list
List<TodoItem>
to access SelectedItem i need to typecast it:
TodoItem i = (TodoItem)TodoListView.SelectedItem;
This added, all errors are gone, app code builds OK.
Btw, unfortunately this approach to get at the item where the Switch has been flipped has proven not working, TodoListView does always return null for SelectedItem, seems that the ListView control doesn't see the button press. Need to find a different way to find the list item beneath the switch to get at the bound row id.

ListBox Index returns negative value

I'm creating a windows phone app using MVVM light pattern. I'm having trouble on my list box as it always returning a negative value (-1) for selected index. Does anyone knows how to resolve it?
here is my code in View Model, do i missed anything?Thanks!
public void OnViewListSelectedItem(SelectionChangedEventArgs e)
{
ListBox lb = new ListBox();
if (e.AddedItems.Count == 1)
{
if (lb.SelectedIndex == 0)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByVendorUrl, UriKind.Relative));
}
if (lb.SelectedIndex == 1)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByVendorUrl, UriKind.Relative));
}
if (lb.SelectedIndex == 2)
{
_navigationService.NavigateTo(new Uri(ViewModelLocator.ByCombinationUrl, UriKind.Relative));
}
}
}
XAML code Here
<ListBox x:Name="lbviewlist">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<Command:EventToCommand Command="{Binding ViewListCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.Items>
<ListBoxItem Content="By Product" FontSize="35" Margin="10,12,12,0"/>
<ListBoxItem Content="By Vendor" FontSize="35" Margin="10,12,12,0"/>
<ListBoxItem Content="By Best Combination" FontSize="35" Margin="10,12,12,0"/>
</ListBox.Items>
</ListBox>
You are creating a new ListBox() (called lb) in your code. You don't populate it, so it will be empty and will always have a SelectedIndex of -1
Then check the 'Source' property of 'e' and cast it to a ListBox
ListBox myList = (ListBox) e.Source;
You can then access the properties on myList.
based on my research SelectedIndex property of listbox is not bindable, when you use the get accessor for the SelectedIndex property, it always returns -1. An attempt to use the set accessor for the SelectedIndex property raises a NotSupportedException. -- MSDN List selectedporperty
I also updated my code since my first code is wrong that creates new listbox and that results to empty/null. Also selectionchanged event does not have a problem to be used as event.
public void method (SelectionChangedEventArgs e)
{
{
if (e.AddedItems.Count == 1)
{
var listBoxItem = (ListBoxItem)e.AddedItems[0];
string _string1 = "Test";
if ((string)listBoxItem.Content == _string1)
{
navigationService.NavigateTo(new Uri(ViewModelLocator.page1, UriKind.Relative));
}
}
}
}
Thats it. Hope it helps! :)

What is the proper way to perform page navigation on ListBox selection changes

I'm trying the MVVM Light Toolkit. Though I still think having multiple ViewModels for such small apps is overkill, I like the concepts. What I still can't quite understand is how (or I should say "what is the recommended way") to navigate from one page to another when the selection changes in a ListBox.
The big problem with this toolkit is that it forces you to learn MVVM via other sources before using it, rather than show you what (its vision of) MVVM is from within the framework, accompanying samples and documentation. Are there samples out there showing the different concepts? And please, no videos.
Have you tried modifying your ListBox ItemTemplate to have each item be a HyperlinkButton and just setting the NavigateURI attribute to the Page you want to navigate to?
I still have not figured out how to do this (navigate to a details page upon selection changed in a listbox) without any codebehind in the view. However, if you are OK with having just a little codebehind in the view here's what I recommend:
<ListBox x:Name="MainListBox" Margin="0,0,-12,0" ItemsSource="{Binding Items}"
SelectionChanged="MainListBox_SelectionChanged"
SelectedItem="{Binding Path=SelectedListItem, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432">
<TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
<TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
First, per above bind to the SelectedItem property of the Listbox with a TwoWay binding to a property in your ViewModel (SelectedListItem in the above).
Then in your codebehind for this page implement the handler for MainListBox_SelectionChanged:
// Handle selection changed on ListBox
private void MainListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// If selected index is -1 (no selection) do nothing
if (MainListBox.SelectedIndex == -1)
return;
// Navigate to the new page
NavigationService.Navigate(new Uri("/DetailsPage.xaml", UriKind.Relative));
}
This is the only codebehind you need in your main view.
In your main ViewModel you need a SelectedListItem property:
public const string SelectedListItemPropertyName = "SelectedListItem";
private ItemViewModel _SelectedListItem;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding
/// </summary>
/// <returns></returns>
public ItemViewModel SelectedListItem
{
get
{
return _SelectedListItem;
}
set
{
if (value != _SelectedListItem)
{
_SelectedListItem = value;
RaisePropertyChanged(SelectedListItemPropertyName);
}
}
}
Now, the trick to getting the context passed to your details page (the context being what list item was selected) you need to setup the DataContext in your Details view:
public DetailsPage()
{
InitializeComponent();
if (DataContext == null)
DataContext = App.ViewModel.SelectedListItem;
}
Hope this helps.
eventually you'll want to do more than just navigate, potentially navigate after setting a custom object.
Here is a MVVM-light way of doing this.
You'll first want to bind your listbox selected item to a property in your viewmodel
<ListBox ItemsSource="{Binding Events}" Margin="0,0,-12,0" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
Declare your SelectedEvent property
public const string SelectedEventPropertyName = "SelectedEvent";
private Event _selectedEvent;
public Event SelectedEvent
{
get {return _selectedEvent;}
set
{
if (_selectedEvent == value)
{
return;
}
var oldValue = _selectedEvent;
_selectedEvent = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(SelectedEventPropertyName, oldValue, value, true);
}
}
You can then define an interaction trigger bound to the tap event
<i:Interaction.Triggers>
<i:EventTrigger EventName="Tap">
<cmd:EventToCommand Command="{Binding EventPageCommand, Mode=OneWay}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
In your viewmodel, define your EventPageCommand as a RelayCommand:
public RelayCommand EventPageCommand { get; private set; }
public MainViewModel()
{
EventPageCommand = new RelayCommand(GoToEventPage);
}
and finally declare your GoToEventPage method
private void GoToEventPage()
{
_navigationService.NavigateTo(new Uri("/EventPage.xaml", UriKind.Relative));
}
note that you can do other actions before navigating to your new page, plus your selected item from your list box is currently set in the property you bound it too.

Manipulating a Storyboard's target object

In the handler for the Completed event of a Storyboard, how do i get the element that the storyboard was being applied to?
My Storyboard is part of an ItemTemplate:
<ListBox x:Name="MyListBox" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="Container" Height="30" >
<Grid.Resources>
<Storyboard x:Name="FadeOut" BeginTime="0:0:7" Completed="FadeOut_Completed">
<DoubleAnimation From="1.0" To="0.0" Duration="0:0:3" Storyboard.TargetName="Container" Storyboard.TargetProperty="Opacity" />
</Storyboard>
</Grid.Resources>
[...snip...]
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
in the Completed event i want to grab the grid called Container so that i can do nasty things with its DataContext. Can this be done, or am i going about it the wrong way?
Thanks :)
The answer to this is that it is not possible - not in Silverlight 3 anyway.
Using a debugger i was able to find a private property of the Storyboard that when i walked it up the object tree i got to the containing template item - however i couldn't touch this from code using reflection due to the security restrictions placed upon silverlight apps (this may well be possible in WPF though).
My eventual solution involved using a Dictionary<Storyboard, Grid>, and a couple of event handlers. With the template i attached a Loaded handler, this means my handler gets called everytime an instance of the template is created and loaded (i.e. for every data item that is bound to the listbox). At this point, i have a reference to the physical instance of the template, so i can search its children for the storyboard:
private void ItemTemplate_Loaded(object sender, RoutedEventArgs e)
{
Storyboard s = getStoryBoard(sender);
if (s != null)
{
if (!_startedStoryboards.ContainsKey(s))
_startedStoryboards.Add(s, (Grid)sender);
}
}
private Storyboard getStoryBoard(object container)
{
Grid g = container as Grid;
if (g != null)
{
if (g.Resources.Contains("FadeOut"))
{
Storyboard s = g.Resources["FadeOut"] as Storyboard;
return s;
}
}
return null;
}
private Dictionary<Storyboard, Grid> _startedStoryboards = new Dictionary<Storyboard, Grid>();
Then when the storyboard's Completed event is fired, i can easily use this dictionary as a lookup to retrieve the item template it was hosted within, and from there i can get the DataContext of the item template and do the nasty things i planned:
private void FadeOut_Completed(object sender, EventArgs e)
{
if (_startedStoryboards.ContainsKey((Storyboard)sender))
{
Grid g = _startedStoryboards[(Storyboard)sender];
if (g.DataContext != null)
{
MyDataItem z = g.DataContext as MyDataItem;
if (z != null)
{
... do my thing ...
}
}
}
}
[Note: this code has been sanitized for public viewing, excuse any small discrepancies or syntactical errors you may spot]

Categories

Resources