I am trying to figure out how the best way to use AutoCompleteBox with MVVM Light.
public ICommand AutoComplete
{
get
{
return new RelayCommand<KeyEventArgs>(e =>
{
var txtBox = e.OriginalSource as TextBox;
if (e.Key == Key.Unknown)
{
return;
}
string autoComplete = txtBox.Text + e.Key;
if (autoComplete.Length >= 3)
{
RestClient c = new RestClient("http://localhost:3333/api/store");
RestRequest r = new RestRequest("/GetStoreNames",Method.GET);
r.AddParameter("Name", autoComplete);
r.AddParameter("Latitude", "49");
r.AddParameter("Longitude", "49");
var d = c.BuildUri(r);
c.ExecuteAsync(r, response2 =>
{
var content = response2.Content;
});
}
});
}
}
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding AutoComplete, Mode=OneWay}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<toolkit:AutoCompleteBox x:Name="acbStore" Margin="154,196,29,0" VerticalAlignment="Top" RenderTransformOrigin="0.6,0.083" Height="162" MinimumPopulateDelay="500"/>
I did the above but there is a couple problems.
Once I get the results back how do I show them in the auto complete area?
How can I delay it from doing to many requests at once? As you can see I don't want to hit the server before 3 characters are entered but after that it is fair game. I am kinda worried that like 20 requests will be done to the server before the 1st request makes it back leading to wasted bandwidth.
I am assuming you are using KeyDown event or similar? That is not the way you want to do it. Instead, bind the AutoCompleteBox Populating event and set the MinimumPrefixLength on your AutoCompleteBox to 3 so thatPopulating is fired only when you have 3+ characters. To show the list retrieved in your control, the list need to be bound to ItemsSource property then a method needs to be called, PopulateComeplte().
You can see my answer here on a similar Question.
However, it is not MVVM friendly since you need to call a method on your AutoCompleteBox to trigger the control to show the list from your webservice. Take a look at this article for a MVVM-friendly approach, scroll down to "Bonus: MVVM-friendly asynchronous filtering" section.
Related
My application viewModel responds to a user clicking a button to see test results:
private void AddDetailRows(List<QuizHistory> quizHistoryList)
{
quizDetails.Children.Clear();
quizDetails.Children.Add(AddData(quizHistoryList));
quizDetails.Children.Add(new LineTemplate());
}
Where quizDetails is the name of an element in the view.
But this doesn't work for me as the view model doesn't know what the view looks like and does not have access to the names of elements.
In a MVVM application, how is this problem solved?
You are completely right, that is not something that ViewModel is responsible of.
So, whatever you want to do with UI is not responsibility of the ViewModel.
If this is really the only option, then you can think of creating boolean properties in your VM and binding them to your views and then changing that boolean from false to true or vice versa on button click command which is binded to your VM.
To simplify it:
MyView.xaml
<StackLayout>
<Button Command="{Binding ShowHideQuizHistoryCommand}" ... />
<StackLayout x:Name="QuizHistory"
IsVisible={Binding ShowQuizHistory }>
//
</StackLayout>
</StackLayout>
MyViewModel.cs
private bool _showQuizHistory ;
public bool ShowQuizHistory
{
get { return _showQuizHistory ; }
set
{
_showQuizHistory = value;
OnPropertyChanged();
}
}
public ICommand ShowHideQuizHistoryCommand => new Command(() =>
{
ShowQuizHistory = !ShowQuizHistory;
});
So, this is just an example based on what you provided in question.
You can also use visual states, converters, triggers and behaviors in order to achieve this, but in my opinion this is the easiest way.
I've come across an issue using the Pivot control with Caliburn Micro and WP8. When I update DisplayName for the child ViewModels (pivot items), the spacing of the pivot headers does not update to reflect this. Thus, pivot headers overlap each other, and it looks very jumbled.
Example:
WP7 Version:
WP8 Version:
This problem only started when we migrated from targetting WP7/WP8 to WP8 only. Does anyone have any ideas about how to get the spacing to update when changing DisplayName, and thus the pivot header text? Thanks!
I know a long time has passed since you asked this, but I got a solution.
Microsoft is promising that this issue is fixed in WP8, but it still isn't.
In order to render the headers properly, you have to change the width of the pivot item.
So here is an example code width x:Name="PivotControl" set for Pivot:
double width = PivotControl.Width;
double width2 = Application.Current.Host.Content.ActualWidth+10;
PivotControl.Width = width2;
await Task.Delay(1);
PivotControl.Width = width;
This will change the width to a bit more than yoru screen width and than after 1ms change it back so the items will be rendered properly and the Layout will be updated (because LayoutUpdate is not working).
Here is better solution
Pivot header template:
<!-- The SizeChanged event is key here. We have to invalidate an ancestor when we get this event. -->
<DataTemplate x:Key="pivotHeaderTemplate">
<TextBlock SizeChanged="OnHeaderSizeChanged" Text="{Binding SomeData}"/>
</DataTemplate>
Pivot:
<phone:Pivot Title="WHATEVER"
DataContext="{StaticResource query}"
ItemsSource="{Binding source}"
HeaderTemplate="{StaticResource pivotHeaderTemplate}"
ItemContainerStyle="{StaticResource pivotItemStyle}"/>
Code-behind:
/// <summary>
/// When something inside of the template changes, then we need to invalidate the measure of the ancestor PivotHeadersControl
/// </summary>
private void OnHeaderSizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement fe = (FrameworkElement)sender;
while (fe != null)
{
if (fe is PivotHeadersControl)
{
fe.InvalidateMeasure();
break;
}
fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
}
}
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.
I have created a Scrollviewer in WP7, which harbors 3 usercontrol, each one of which hold as their content XAML created UserControls. This works fine. This scrollviewer should be able to scroll between these items, but make this not possible for the user to scroll. So when an item in one of these contents are clicked upon, the scrollviewer slides left or right depending on the item selected, and bring into view one of the other usercontrols. I use a mediator to accomplish this:
<Grid.Resources>
<Storyboard x:Name="ItemAnimation">
<DoubleAnimation x:Name="ItemAnimationContent"
Storyboard.TargetName="Mediator"
Storyboard.TargetProperty="ScrollableWidthMultiplier"/>
</Storyboard>
</Grid.Resources>
<ScrollViewer Name="ScrollableItemPanel"
Grid.Row="2"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Orientation="Horizontal">
<UserControl Name="NewsListBoxControl" Width="480" />
<UserControl Name="DetailedItemControl" Width="480"/>
<UserControl Name="ExternalBrowserItemControl" Width="480"/>
</StackPanel>
</ScrollViewer>
<local:ScrollableItemAnimationMediator x:Name="Mediator"
ScrollViewer="{Binding ElementName=ScrollableItemPanel}"/>
In basic, this works fine too, I can navigate between the items, and load upon them the content as usercontrols. But the problem lies in granting the user the abillity to scroll. Before the item scrolls, I set the hittestvisibilty to true, and the horizontalscrollbarvisibility to visible. After the animation is done, I want to grant back the hittestvisibility and set the horizontalscrollbarvisibility to Disabled again. This latter is where the problem is: when I set the horizontalscrollbarvisibility to Disabled, the scrollviewer automatically brings back into view the first of three items in the stackpanel. How can I stop this? This is the code I use to scroll the mediator:
private void CreateDetailedArticleItem( Dictionary<string, string> itemQuery )
{
_articleDetailPage.ItemQuery = itemQuery;
DetailedItemControl.Content = _articleDetailPage as UserControl;
Animate( _articleDetailPage, 0.0f, 0.5f, 250 );
}
private void Animate( IContentControl control, float from, float to, double milliseconds )
{
//this eventhandler will fire when the animation has completed
EventHandler handler = null;
//we take away the User Input just for the moment, so that we can animate without the user interfering. Also, we make horizontalScroll Visible
IsUserEnabled = false;
//we then set the content of the animation. Where from will it move, towards where and in what duration?
ItemAnimationContent.From = from;
ItemAnimationContent.To = to;
ItemAnimationContent.Duration = TimeSpan.FromMilliseconds( milliseconds );
//we start the animation
ItemAnimation.Begin( );
//we tell the new control that it will appear soon, so it can load its main content
control.ViewWillAppear( );
//also, we tell the currentcontrol that it will disappear soon, so it can unload its content and eventhandlers and so on
CurrentControl.ViewWillDisAppear( );
//the handler is a delegate. This way, it becomes rather easy and clean to fire the completed event, without creating a strong reference ( well, actually,
//we do create a strong reference, but as soon as it is fired, we remove it again, shhhh! ).
handler = delegate( object sender, EventArgs e )
{
//as stated, we remove the eventlistener again, so it won't keep firing all the time
ItemAnimation.Completed -= handler;
//after the animation, we tell the new control that it is now in screen, and can start downloading its data
control.ViewDidAppear( );
//at the same time, the "current" control has fully moved out of view, so it can now fully unload all its content.
CurrentControl.ViewDidDisAppear( );
//now, all we have to do is to make sure that the next time an item is being loaded, the new content is spoken to, not the old one
CurrentControl = control;
//and finally, enable the users input again, and remove the horizontal scrollbarvisibility
IsUserEnabled = true;
};
ItemAnimation.Completed += handler;
}
private bool IsUserEnabled
{
set
{
//when the user can control the scrollviewer, then the horizontal scrollvisibility is disabled, so that the user cannot move horizontally,
//otherwise, so we only make it visible when the program needs to animate.
ScrollableItemPanel.IsHitTestVisible = value;
ScrollableItemPanel.HorizontalScrollBarVisibility = value ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Visible;
}
}
I had already asked this question, then regarded it as answered, as I thought it to be answered, namely using ScrollbarVisibility.Hidden instead of ScrollbarVisibility.Disabled, only the scrollbarvisibility stays visible this way, and the user can still scroll. Is there a native way to deal with this problem?
Any help would be greatly appreciated. Greetz
Rather than fight the behavior of the native control it may be easier to just manipulate the position of items yourself using a custom control (wrapping your other controls) which animates between different visual states (adjust the translate transform) depending on the "selected" item.
I have a command wired to the event such that it does fire, but what I get in the CommandParameter is the previously selected item, or maybe it's the selected item before the SelectionChanged completes.
Either way, not sure what to change to get the newly selected item from the event.
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand
Command="{Binding Main.SelectedRecordCommand, Source={StaticResource Locator}}"
CommandParameter="{Binding SelectedItem, ElementName=listBillingRecords}"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
Thanks
Is it worth using a trigger? If whatever your XAML element is for the collection (listbox, grid, etc) is bound to a property exposing a collection on your viewmodel, you can leverage both databinding and the built-in MVVM Light messenger to notify you of a property change with both old and new values in a more MVVM-friendly way. This example isn't necessarily WP7-specific, but I think it would work the same.
For example, this might be the databound collection:
public const string BillingRecordResultsPropertyName = "BillingRecordResults";
private ObservableCollection<BillingRecord> _billingRecordResults = null;
public ObservableCollection<BillingRecord> BillingRecordResults
{
get
{
return _billingRecordResults;
}
set
{
if (_billingRecordResults == value)
{
return;
}
var oldValue = _billingRecordResults;
_billingRecordResults = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(BillingRecordResultsPropertyName, oldValue, value, true);
}
}
I like to expose a property on my ViewModel that is a "selected item" of whatever collection I'm exposing. So, to the ViewModel, I would add this property using the MVVMINPC snippet:
public const string SelectedBillingRecordPropertyName = "SelectedBillingRecord";
private BillingRecord _selectedBillingRecord = null;
public BillingRecord SelectedBillingRecord
{
get
{
return _selectedBillingRecord;
}
set
{
if (_selectedBillingRecord == value)
{
return;
}
var oldValue = _selectedBillingRecord;
_selectedBillingRecord = value;
// Update bindings and broadcast change using GalaSoft.MvvmLight.Messenging
RaisePropertyChanged(SelectedBillingRecordPropertyName, oldValue, value, true);
}
}
Now, if you bind the SelectedItem of the XAML element to this exposed property, it will populate when selected in the View via databinding.
But even better, when you leverage the snippet MVVMINPC, you get to choose whether or not to broadcast the results to anyone listening. In this case, we want to know when the SelectedBillingRecord property changes. So, you can have this in the constructor for your ViewModel:
Messenger.Default.Register<PropertyChangedMessage<BillingRecord>>(this, br => SelectedRecordChanged(br.NewValue));
And elsewhere in your ViewModel, whatever action you want to have happen:
private void SelectedRecordChanged(BillingRecord br)
{
//Take some action here
}
Hope this helps...
I have seen the same issue and found that SelectedItem is the correct implementation.