I have a SearchResult class that binds to a ListView. What I want to do specifically is highlight the snippet inside the search result text that matches the query the user entered.
The relevant XAML looks something like this (omitting the fluff):
<DataTemplate>
<StackPanel>
<!-- Search result -->
<RichTextBlock>
<!-- Would this idea work? -->
<RichTextBlock.TextHighlighters>
<TextHighlighter>
<TextHighlighter.Ranges>
<!-- Add the bound range here-->
<!-- {Binding Range} or text highlighter or something -->
</TextHighlighter.Ranges>
</TextHighlighter>
</RichTextBlock.TextHighlighters>
<Paragraph>
<Run Text="{Binding Text}"></Run>
</Paragraph>
</RichTextBlock>
</StackPanel>
</DataTemplate>
I can add whatever property from the SearchResult class, be it a TextHighlighter or a TextRange. I just don't know whether the XAML syntax allows plugging in that value.
I've also thought of doing this in code, but I do want to keep the search item template inside the XAML, and not put it in C#. However, it would be possible to do something like lvSearchResults.Items[i]... or whatever it takes to put in the highlighter or range. I just can't figure out the correct method at the moment.
If you are planning to create a locally highlighted search result list, you can try this way:
Create a search result class
public class SearchResult
{
public string DisplayText { get; set; }
public string HighlightText { get; set; }
}
Create a UserControl to show the result
SearchResultBlock.xaml
<Grid>
<TextBlock x:Name="ResultBlock" TextWrapping="Wrap" MaxLines="2"
TextTrimming="CharacterEllipsis"/>
</Grid>
SearchResultBlock.xaml.cs
public sealed partial class SearchResultBlock : UserControl
{
public SearchResultBlock()
{
this.InitializeComponent();
}
public SearchResult Result
{
get { return (SearchResult)GetValue(ResultProperty); }
set { SetValue(ResultProperty, value); }
}
public static readonly DependencyProperty ResultProperty =
DependencyProperty.Register("Result", typeof(SearchResult), typeof(SearchResultBlock), new PropertyMetadata(null,new PropertyChangedCallback(Result_Changed
private static void Result_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(e.NewValue!=null && e.NewValue is SearchResult data)
{
var instance = d as SearchResultBlock;
instance.ResultBlock.Inlines.Clear();
var sp = data.DisplayText.Split(data.HighlightText);
instance.ResultBlock.Inlines.Add(new Run { Text = sp.First() });
instance.ResultBlock.Inlines.Add(new Run { Text = data.HighlightText, Foreground = new SolidColorBrush(Colors.Red) });
if (sp.Length > 1)
instance.ResultBlock.Inlines.Add(new Run { Text = sp.Last() });
}
}
}
Use it in DataTemplate
<DataTemplate x:DataType="SearchResult" x:Key="ResultItemTemplate">
<SearchResultBlock Result="{Binding}"/>
</DataTemplate>
By string splitting, create different types of Runs and merge them in the TextBlock. This can also achieve the highlighting effect.
Best regards.
I'm developing a Windows 8.1 Universal App using the MVVM Light Toolkit.
I have a ListView
<ListView
x:Name="MyListView"
ItemsSource="{Binding ModelCollection, Mode=TwoWay}"
SelectedItem="{Binding CurrentModelItem, Mode=TwoWay}"
and my ViewModel is as it follows:
public class PageViewModel : ViewModelBase
{
private ObservableCollection<MyViewModel> _modelCollection =
new ObservableCollection<MyViewModel>();
public ObservableCollection<MyViewModel> ModelCollection
{
get { return _modelCollection ; }
set { Set(() => ModelCollection , ref _modelCollection , value); }
}
private MyViewModel _currentModelItem;
public InspectionDiscrepancyViewModel CurrentModelItem
{
get { return _currentModelItem; }
set { Set(() => CurrentModelItem, ref _currentModelItem, value); }
}
If I let the CurrentModelItem item at load time it works 'fine' but if I try to set the CurrentModelItem in the LoadData Method -that populates the ObservableCollection- to the first ModelCollection Item
CurrentModel = ModelCollection.FirstOrDefault();
I get the following error on the Set() Method of the MVVM Light Toolkit:
An exception of type 'System.ArgumentException' occurred in mscorlib.dll but was not handled in user code
Additional information: Value does not fall within the expected range.
I've seen that doing this should be very straightforward, what am I doing wrong here?
I'm working on a Xamarin.Forms app using a page that displays a map.
The XAML is:
<maps:Map x:Name="Map">
...
</maps:Map>
I know that the map can be accessed from the page's code-behind like this:
var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin
{
Label = "Xamarin",
Position = position
});
But because this code would break the app's MVVM architecture, I'd rather like to access the Map object from my ViewModel, not directly from the View/page - either using it directly like in the above code or by databinding to its properties.
Does anybody know a way how this can be done?
If you don't want to break the MVVM pattern and still be able to access your Map object from the ViewModel then you can expose the Map instance with a property from your ViewModel and bind to it from your View.
Your code should be structured like described here below.
The ViewModel:
using Xamarin.Forms.Maps;
namespace YourApp.ViewModels
{
public class MapViewModel
{
public MapViewModel()
{
Map = new Map();
}
public Map Map { get; private set; }
}
}
The View (in this example I'm using a ContentPage but you can use whatever you like):
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="YourApp.Views.MapView">
<ContentPage.Content>
<!--The map-->
<ContentView Content="{Binding Map}"/>
</ContentPage.Content>
</ContentPage>
I didn't show how, but the above code snipped can only work when the ViewModel is the BindingContext of your view.
What about creating a new Control say BindableMap which inherits from Map and performs the binding updates which the original Map lacks internally. The implementation is pretty straightforward and I have included 2 basic needs; the Pins property and the current MapSpan. Obviously, you can add your own special needs to this control. All you have to do afterward is to add a property of type ObservableCollection<Pin> to your ViewModel and bind it to the PinsSource property of your BindableMap in XAML.
Here is the BindableMap:
public class BindableMap : Map
{
public BindableMap()
{
PinsSource = new ObservableCollection<Pin>();
}
public ObservableCollection<Pin> PinsSource
{
get { return (ObservableCollection<Pin>)GetValue(PinsSourceProperty); }
set { SetValue(PinsSourceProperty, value); }
}
public static readonly BindableProperty PinsSourceProperty = BindableProperty.Create(
propertyName: "PinsSource",
returnType: typeof(ObservableCollection<Pin>),
declaringType: typeof(BindableMap),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
validateValue: null,
propertyChanged: PinsSourcePropertyChanged);
public MapSpan MapSpan
{
get { return (MapSpan)GetValue(MapSpanProperty); }
set { SetValue(MapSpanProperty, value); }
}
public static readonly BindableProperty MapSpanProperty = BindableProperty.Create(
propertyName: "MapSpan",
returnType: typeof(MapSpan),
declaringType: typeof(BindableMap),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
validateValue: null,
propertyChanged: MapSpanPropertyChanged);
private static void MapSpanPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisInstance = bindable as BindableMap;
var newMapSpan = newValue as MapSpan;
thisInstance?.MoveToRegion(newMapSpan);
}
private static void PinsSourcePropertyChanged(BindableObject bindable, object oldvalue, object newValue)
{
var thisInstance = bindable as BindableMap;
var newPinsSource = newValue as ObservableCollection<Pin>;
if (thisInstance == null ||
newPinsSource == null)
return;
UpdatePinsSource(thisInstance, newPinsSource);
newPinsSource.CollectionChanged += thisInstance.PinsSourceOnCollectionChanged;
}
private void PinsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdatePinsSource(this, sender as IEnumerable<Pin>);
}
private static void UpdatePinsSource(Map bindableMap, IEnumerable<Pin> newSource)
{
bindableMap.Pins.Clear();
foreach (var pin in newSource)
bindableMap.Pins.Add(pin);
}
}
Notes:
I have omitted the using statements and namespace declaration for the sake of simplicity.
In order for our original Pins property to be updated as we add members to our bindable PinsSource property, I declared the PinsSource as ObservableCollection<Pin> and subscribed to its CollectionChanged event. Obviously, you can define it as an IList if you intend to only change the whole value of your bound property.
My final word regarding the 2 first answers to this question:
Although having a View control as a ViewModel property exempts us from writing business logic in code behind, but it still feels kind of hacky. In my opinion, the whole point of (well, at least a key point in) the VM part of the MVVM is that it is totally separate and decoupled from the V. Whereas the solution provided in the above-mentioned answers is actually this:
Insert a View Control into the heart of your ViewModel.
I think this way, not only you break the MVVM pattern but also you break its heart!
I have two options which worked for me and which could help you.
You could either add a static Xamarin.Forms.Maps Map property to your ViewModel and set this static property after setting the binding context, during the instantiation of your View, as show below:
public MapsPage()
{
InitializeComponent();
BindingContext = new MapViewModel();
MapViewModel.Map = MyMap;
}
This will permit you to access your Map in your ViewModel.
You could pass your Map from your view to the ViewModel during binding, for example:
<maps:Map
x:Name="MyMap"
IsShowingUser="true"
MapType="Hybrid" />
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Button x:Name="HybridButton" Command="{Binding MapToHybridViewChangeCommand}"
CommandParameter="{x:Reference MyMap}"
Text="Hybrid" HorizontalOptions="Center" VerticalOptions="Center" Margin="5"/>`
And get the Map behind from the ViewModel's Command.
Yes, Map.Pins is not bindable, but there is ItemsSource, which is easy to use instead.
<maps:Map ItemsSource="{Binding Locations}">
<maps:Map.ItemTemplate>
<DataTemplate>
<maps:Pin Position="{Binding Position}"
Label="{Binding Name}"
Address="{Binding Subtitle}" />
So, just for the pins, MVVM can be done without any custom control.
But Map.MoveToRegion() (and Map.VisibleRegion to read) is still open. There should be a way to bind them. Why not both in a single read/write property? (Answer: because of an endless loop.)
Note: if you need Map.MoveToRegion only once on start, the region can be set in the constructor.
I don't think Pins is a bindable property on Map, you may want to file feature request at Xamarin's Uservoice or the fourm here: http://forums.xamarin.com/discussion/31273/
It is not ideal, but you could listen for the property changed event in the code behind and then apply the change from there. Its a bit manual, but it is doable.
((ViewModels.YourViewModel)BindingContext).PropertyChanged += yourPropertyChanged;
And then define the "yourPropertyChanged" method
private void yourPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == "YourPropertyName")
{
var position = new Position(37.79762, -122.40181);
Map.MoveToRegion(new MapSpan(position, 0.01, 0.01));
Map.Pins.Add(new Pin
{
Label = "Xamarin",
Position = position
});
}
}
I've created a UserControl, LiveTile.xaml (streamlined for brevity):
<UserControl
x:Class="Weathercast.Core.LiveTile"
xmlns:local="clr-namespace:Weathercast.Core">
<StackPanel
x:Name="LayoutRoot">
<StackPanel
x:Name="TileRegularFront"
Width="336"
Height="336"
Background="Red">
<TextBlock Text="{Binding TempCurrentHour}"/>
</StackPanel>
</UserControl>
Its code behind, LiveTile.xaml.cs:
public partial class LiveTile : UserControl
{
public LiveTile()
{
InitializeComponent();
LiveTileViewModel vm = new LiveTileViewModel();
this.DataContext = vm;
}
}
Its view model, LiveTileViewModel.cs:
public class LiveTileViewModel : ObservableObject
{
/** PROPERTIES **/
private string _tempCurrentHour;
public string TempCurrentHour
{
get { return _tempCurrentHour; }
set
{
_tempCurrentHour = value;
RaisePropertyChanged("TempCurrentHour");
}
}
/** CONSTRUCTOR **/
public LiveTileViewModel()
{
this.TempCurrentHour = "15"; // dummy value set
}
}
ObservableObject.cs:
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The problem is the value I'm binding ("TempCurrentHour") is not being displayed. Any ideas on what I need to do in order to get the User Control's View Model's binded property value to display? Based on my research, I believe binding a value to a User Control is less straightforward than normal. However I can't get my head around what needs to be done to get the User Control to detect binded property values.
UPDATE: Just to be clear, the LiveTile class is in a Library project for my solution. An instance of it is created when the user toggles on the Live Tile via the Settings PhoneApplicationPage located in the Windows Phone App project in the solution. This is the event handler that instantiates a LiveTile in Settings.xaml.cs:
private void LiveTile_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Weathercast.Core.LiveTile l = new Weathercast.Core.LiveTile();
l.CreateOrUpdateTile(1);
}
The CreateOrUpdateTile method is doing its job correctly and takes the user back to their phone's Start screen with the Live Tile now there. This is its code in any case (I'm using Telerik's LiveTileHelper):
RadFlipTileData tileData = new RadFlipTileData()
{
VisualElement = this.TileRegularFront,
BackVisualElement = this.TileRegularBack,
SmallVisualElement = this.TileSmall
};
// Tile's uri has a unique paramater which is the location Id of the currently viewed location.
Uri tileUri = new Uri("/MainPage.xaml?locationId=" + locationId, UriKind.RelativeOrAbsolute);
// If the tile for this location previously existed, delete it before adding it anew.
ShellTile tile = LiveTileHelper.GetTile(tileUri);
if (tile != null)
{
tile.Delete();
}
// Create brand new tile for location if didn't have tile previously or fresh tile if it did.
LiveTileHelper.CreateOrUpdateTile(tileData, tileUri, true);
this.tile = tileData;
// Add the Background Agent for this tile with the agent's name
// unique for the location.
AddAgent("PeriodicTaskForLocation" + locationId);
I should note another problem I'm having, that may or may not be related to the original issue, is that the background property I'm setting in LiveTile.xaml for the StackPanel or LayoutRoot element even is being neglected and the Live Tile that's being added to the Start screen is transparent (black).
I have two view and their corresponding ViewModels and i want to send text from one view to another using MVVM Light as follows
in first viewmodel i am calling the following method
public void NavigatePage()
{
string temp = "temp value";
Messenger.Default.Send("temp");
Frame frame = Window.Current.Content as Frame;
if (frame != null) frame.Navigate(typeof(MyPage), temp);
}
while in page 2 view model i am having the following code
public MyViewModel()
{
Messenger.Default.Register<string>(this, MessageReceived);
}
private string test;
public string Test
{
get { return test; }
set { test = value; RaisePropertyChanged("Test");}
}
private void MessageReceived(string message)
{
Test = message;
}
when i debug my code the ctor of this viewmodel is getting called but the MessageReceived is not getting called hence property Test is never getting set, I am missing something, please help
Is the SecondViewModel actually created before you send the message? You can specify this in the ViewModelLocator class.
In the locator you have to register your viewmodel and CREATE it when the applications starts.
Like this:
SimpleIoc.Default.Register<SecondViewModel>(true);
With the true parameter the SecondViewModel will be created when the application is started! :)