CollectionView not loading Xamarin.Forms on load - c#

Need help regarding CollectionView not loading data from API upon loading. I need to pull-down refresh first before the data will show. I'm doing it MVVM.
Below is my XAML:
<RefreshView x:DataType="local:MainPageViewModel" Command="{Binding LoadReleaseDocuments}"
IsRefreshing="{Binding IsRefreshing ,Mode=OneWay}"
RefreshColor="#FFFF7F50">
<CollectionView x:Name="SuccessList"
ItemsSource="{Binding ReleasedDocuments}"
SelectionMode="Single" >
...
</CollectionView>
</RefreshView>
And this is my code behind:
public ObservableCollection<Release> ReleasedDocuments { get; }
public MainPageViewModel()
{
// collection
ReleasedDocuments = new ObservableCollection<Release>();
DeliveredDocuments = new ObservableCollection<Deliver>();
CallNow = new Command<Deliver>(Call_Now);
//Load
LoadDocuments = new Command(ExecuteLoadItemsCommand);
LoadReleaseDocuments = new Command(ExecuteCommand);
}
And below code is where I get my data thru API calls
void ExecuteCommand()
{
bool forceRefresh = true;
if (IsRefreshing)
return;
IsRefreshing = true;
Device.BeginInvokeOnMainThread(async () =>
{
try
{
ReleasedDocuments.Clear();
switch (Application.Current.Properties["Position"])
{
case string a when a.Contains("Courier"):
var items = await DataStore.GetItemsAsync(forceRefresh, Application.Current.Properties["Position"].ToString(), "tmdm");
items = items.Where(ab => ab.TMNo != null).Where(ac => ac.TMNo.Contains("DM"));
var sortedItems = items.OrderByDescending(c => c.OrderDate);
CourierDMData(sortedItems);
break;
}
}
catch (Exception ex)
{
IsBusy = false;
IsRefreshing = false;
...
}
finally
{
IsBusy = false;
IsRefreshing = false;
}
});
IsRefreshing = false;
}
And inserting it to ObservableCollection
void CourierDMData(IOrderedEnumerable<Summary> sortedItems)
{
ReleasedDocuments.Clear();
foreach (var itemx in sortedItems)
{
if (itemx.statusId == 0)
{
ReleasedDocuments.Add(new Release()
{
Id = itemx.Id,
....
});
}
}
CountRelease = ReleasedDocuments.Count;
}
When debugging, I can get the CountRelease = ReleasedDocuments.Count; value (count) it is displaying correctly the value, but the CollectionView is not showing anything until I refresh.

I'm usually doing a work around and call refresh with the PageAppearingEvent and use Xamarin Community Toolkit EventToCommandBehavior to call a function which calls the refresh function with a small delay if necessary. This way I don't have to manually refresh each time I open the page.
XAML example:
<ContentPage.Behaviors>
<xct:EventToCommandBehavior
EventName="Appearing"
Command="{Binding AppearingCommand}"/>
</ContentPage.Behaviors>
MVVM example:
public MyViewModel() //constructor
{
AppearingCommand = new Command(OnAppearing);
}
public ICommand AppearingCommand { get; }
private void OnAppearing()
{
await System.Threading.Tasks.Task.Delay(int delay); //only if necessary because of initialization
Refresh(); //Or else set your public properties
}

Related

WebView and error management, what's wrong?

I'm working on a Xamarin.Forms app where I need to integrate a WebView to manage booking through an external URL.
So basically I've did this in my view:
<WebView x:Name="webView" Source="{Binding BookingUrl}"
WidthRequest="1000" HeightRequest="1000">
I would like to manage some errors that the users could encounter while opening this page: no internet access, timeout, unavailable server,...
For this I've used EventToCommandBehaviorto acess to the events Navigating and Navigating in the ViewModel.
So my XAML looks like this:
<WebView x:Name="webView" Source="{Binding AvilaUrlBooking}"
WidthRequest="1000" HeightRequest="1000">
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
Command="{Binding NavigatingCommand}" />
<behaviors:EventToCommandBehavior
EventName="Navigated"
Command="{Binding NavigatedCommand}" />
</WebView.Behaviors>
</WebView>
And the ViewModel is like this:
public ICommand NavigatingCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatingAsync(x);
}
});
}
}
private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
{
if (!IsConnected)
ServiceErrorKind = ServiceErrorKind.NoInternetAccess;
IsBusy = true;
return Task.CompletedTask;
}
public ICommand NavigatedCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatedAsync(x);
}
});
}
}
private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
{
IsBusy = false;
IsFirstDisplay = false;
switch (eventArgs.Result)
{
case WebNavigationResult.Cancel:
// TODO - do stuff here
break;
case WebNavigationResult.Failure:
// TODO - do stuff here
break;
case WebNavigationResult.Success:
// TODO - do stuff here
break;
case WebNavigationResult.Timeout:
// TODO - do stuff here
break;
default:
// TODO - do stuff here
break;
}
return Task.CompletedTask;
}
bool isFirstDisplay;
public bool IsFirstDisplay
{
get { return isFirstDisplay; }
set { SetProperty(ref isFirstDisplay, value); }
}
public BookingViewModel()
{
_eventTracker = new AppCenterEventTracker();
IsFirstDisplay = true;
Title = "Booking";
IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
Connectivity.ConnectivityChanged += OnConnectivityChanged;
}
If I use the right URL, all works fine on iOS and Android.
However, if I use a "wrong" URL (with missing char for example), this is only working on Android: the case WebNavigationResult.Failure is catched in WebViewNavigatedAsync(), but I don't enter in WebViewNavigatedAsync() on iOS.
=> is this normal?
I've implemented a "Refresh" button to manage the "No Internet access" error. This button is accessible through a ToolBarItem, it's like this in the ViewModel:
public void Refresh(object sender)
{
try
{
var view = sender as Xamarin.Forms.WebView;
view.Reload();
}
catch (Exception ex)
{
}
}
But in these case too, I have 2 different behaviours after having activated the Airplane mode:
on iOS, when there is no internet access: I don't enter in WebViewNavigatedAsync(), even if the internet access is available again and I click on the "Refresh" button, I only pass by the WebViewNavigatingAsync()
on Android, when there is no internet access: I well enter in WebViewNavigatedAsync(), and when the internet access is available again and I click on the "Refresh" button, I pass both by the WebViewNavigatingAsync() and WebViewNavigatedAsync()
=> is this normal? Is there a proper way to manager this?
is this normal?
Based on my test. Yes, I got the same result, if we input an error url, the webview always be white-empty view in iOS. so the NavigatedCommand cannot be executed.
If we use correct url, the webview could excute the NavigatedCommand, and running result like following screenshot.
Is there a proper way to manager this?
We can use custom renderer for webview in iOS to handle this situation.
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace MyCarsourlView.iOS
{
[Obsolete]
class CustomWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement == null) { Delegate = new CustomWebViewDelegate(); }
}
}
}
internal class CustomWebViewDelegate : UIWebViewDelegate
{
#region Event Handlers
public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
{
//Could add stuff here to redirect the user before the page loads, if you wanted to redirect the user you would do the redirection and then return false
return true;
}
public override void LoadFailed(UIWebView webView, NSError error)
{
Console.WriteLine("\nIn AppName.iOS.CustomWebViewRenderer - Error: {0}\n", error.ToString()); //TODO: Do something more useful here
//Here, you can test to see what kind of error you are getting and act accordingly, either by showing an alert or rendering your own custom error page using basic HTML
}
public override void LoadingFinished(UIWebView webView)
{ //You could do stuff here such as collection cookies form the WebView }
#endregion
}
}
If I input wrong url, LoadFailed could be executed.
I've found another approach that seems to work, based on the following links:
It's probably not perfect, especially as I need access to the required Events from the ViewModel.
So I've created a CustomWebView control that inherits from WebView:
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public CustomWebViewErrorKind ErrorKind { get; set; }
public event EventHandler LoadingStart;
public event EventHandler LoadingFinished;
public event EventHandler LoadingFailed;
/// <summary>
/// The event handler for refreshing the page
/// </summary>
public EventHandler OnRefresh { get; set; }
public void InvokeCompleted()
{
if (this.LoadingFinished != null)
{
ErrorKind = WebViewErrorKind.None;
this.LoadingFinished.Invoke(this, null);
}
}
public void InvokeStarted()
{
if (this.LoadingStart != null)
{
ErrorKind = WebViewErrorKind.None;
this.LoadingStart.Invoke(this, null);
}
}
public void InvokeFailed(CustomWebViewErrorKind errorKind)
{
if (this.LoadingFailed != null)
{
ErrorKind = errorKind;
this.LoadingFailed.Invoke(this, null);
}
}
/// <summary>
/// Refreshes the current page
/// </summary>
public void Refresh()
{
OnRefresh?.Invoke(this, new EventArgs());
}
}
Then I've the CustomWkWebViewRenderer that customizes the behavior of the CustomWebView:
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWkWebViewRenderer))]
namespace MyProject.iOS.Renderers
{
public class CustomWkWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
public CustomWkWebViewRenderer()
{
Debug.WriteLine($"CustomWkWebViewRenderer - Ctor");
}
WKWebView webView;
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged()");
if (Control == null)
{
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - Control == null");
webView = new WKWebView(Frame, new WKWebViewConfiguration()
{
MediaPlaybackRequiresUserAction = false
});
webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
SetNativeControl(webView);
Element.OnRefresh += (sender, ea) => Refresh(sender);
}
if (e.NewElement != null)
{
Debug.WriteLine($"CustomWkWebViewRenderer - OnElementChanged() - e.NewElement != null");
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
webView.NavigationDelegate = new DisplayWebViewDelegate(Element);
SetNativeControl(webView);
}
}
private void Refresh(object sender)
{
Debug.WriteLine($"CustomWkWebViewRenderer - Refresh()");
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
}
}
}
I also have the CustomWkWebViewNavigationDelegate that implements the WKNavigationDelegate for this renderer:
public class CustomWkWebViewNavigationDelegate : WKNavigationDelegate
{
private CustomWebView element;
public CustomWkWebViewNavigationDelegate(CustomWebView element)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - Ctor");
this.element = element;
}
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFinishNavigation");
element.InvokeCompleted();
//base.DidFinishNavigation(webView, navigation);
}
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidStartProvisionalNavigation");
element.InvokeStarted();
//base.DidStartProvisionalNavigation(webView, navigation);
}
[Export("webView:didFailProvisionalNavigation:withError:")]
public override void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
{
Debug.WriteLine($"CustomWkWebViewNavigationDelegate - DidFailProvisionalNavigation - error : {error}");
var errorKind = CustomWebViewErrorKind.None;
switch (error.Code)
{
case -1009: // no internet access
{
errorKind = CustomWebViewErrorKind.NoInternetAccess;
break;
}
case -1001: // timeout
{
errorKind = CustomWebViewErrorKind.Timeout;
break;
}
case -1003: // server cannot be found
case -1100: // url not found on server
default:
{
errorKind = CustomWebViewErrorKind.Failure;
break;
}
}
element.InvokeFailed(errorKind);
//base.DidFailProvisionalNavigation(webView, navigation, error);
}
}
There is a CustomWebViewErrorKind enum that will allow me to implement a common error management in the ViewModel:
public enum CustomWebViewErrorKind
{
None = 0,
NoInternetAccess = 1,
Failure = 2,
Timeout = 3,
Cancel = 8,
Other = 9
}
To access to the Events from the ViewModel, I use a EventToCommandBehavior like described there
So, I've exposed all the Commands from the View like this:
<controls:CustomWebView x:Name="webView"
Source="{Binding MyUrlBooking}"
Uri="{Binding MyUrlBooking}"
WidthRequest="1000" HeightRequest="1000">
<WebView.Behaviors>
<behaviors:EventToCommandBehavior
EventName="Navigating"
Command="{Binding NavigatingCommand}" />
<behaviors:EventToCommandBehavior
EventName="Navigated"
Command="{Binding NavigatedCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingStart"
Command="{Binding LoadingStartCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingFinished"
Command="{Binding LoadingFinishedCommand}" />
<behaviors:EventToCommandBehavior
EventName="LoadingFailed"
Command="{Binding LoadingFailedCommand}"
CommandParameter="{x:Reference webView}"
/>
</WebView.Behaviors>
</controls:CustomWebView>
And finally, in my ViewModel I do this for the Android part:
public ICommand NavigatingCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatingEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatingAsync(x);
}
});
}
}
private Task WebViewNavigatingAsync(WebNavigatingEventArgs eventArgs)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatingAsync()");
IsBusy = true;
return Task.CompletedTask;
}
public ICommand NavigatedCommand
{
get
{
return new Xamarin.Forms.Command<WebNavigatedEventArgs>(async (x) =>
{
if (x != null)
{
await WebViewNavigatedAsync(x);
}
});
}
}
private Task WebViewNavigatedAsync(WebNavigatedEventArgs eventArgs)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync()");
IsBusy = false;
switch (eventArgs.Result)
{
case WebNavigationResult.Cancel:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Cancel");
ErrorKind = CustomWebViewErrorKind.Cancel;
break;
case WebNavigationResult.Failure:
default:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure");
IsConnected = Connectivity.NetworkAccess == NetworkAccess.Internet;
if (IsConnected)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : Failure");
ErrorKind = CustomWebViewErrorKind.Failure;
}
else
if (IsConnected)
{
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Failure : NoInternetAccess");
ErrorKind = CustomWebViewErrorKind.NoInternetAccess;
}
break;
case WebNavigationResult.Success:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Success");
ErrorKind = CustomWebViewErrorKind.None;
IsFirstDisplay = false;
break;
case WebNavigationResult.Timeout:
Debug.WriteLine($"BookingViewModel - WebViewNavigatedAsync() - Timeout");
ErrorKind = CustomWebViewErrorKind.Timeout;
break;
}
return Task.CompletedTask;
}
And I do this for the iOS part:
public ICommand LoadingStartCommand
{
get
{
return new Xamarin.Forms.Command(async () =>
{
await WebViewLoadingStartAsync();
});
}
}
private Task WebViewLoadingStartAsync()
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingStartAsync()");
IsBusy = true;
return Task.CompletedTask;
}
public ICommand LoadingFinishedCommand
{
get
{
return new Xamarin.Forms.Command(async () =>
{
await WebViewLoadingFinishedAsync();
});
}
}
private Task WebViewLoadingFinishedAsync()
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingFinishedAsync()");
IsBusy = false;
return Task.CompletedTask;
}
public ICommand LoadingFailedCommand
{
get
{
return new Xamarin.Forms.Command<object>(async (object sender) =>
{
if (sender != null)
{
await WebViewLoadingFailedAsync(sender);
}
});
}
}
private Task WebViewLoadingFailedAsync(object sender)
{
Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync()");
var view = sender as CustomWebView;
var error = view.ErrorKind;
Debug.WriteLine($"BookingViewModel - WebViewLoadingFailedAsync() - error : {error}");
IsBusy = false;
return Task.CompletedTask;
}
Like this I'm able to manage errors, retry and refresh from the ViewModel, even if it's probably not the better solution...

"Must create DependencySource on same Thread as the DependencyObject" when changing SelectedItem

I have an error "Must create DependencySource on same Thread as the DependencyObject" in my project.
My comment is used to load a file and create a list. This list is bind to a ListBox. AL was working good. But I created a Task to load (load can be long). Now I have this error. I don't understand why it occurs.
There is my code :
MainView.xaml:
<ListBox ItemsSource="{Binding Results}"
SelectedItem="{Binding SelectedItem}">
<ListBox.InputBindings>
<KeyBinding Command="{Binding RemoveCommand}"
Key="Delete"/>
</ListBox.InputBindings>
</ListBox>
<Button Grid.Row="1" Grid.Column="0"
Style="{StaticResource StyleButton}"
Command="{Binding LoadCommand}"
Content="Open result"/>
MainViewModel:
#region Fields/Properties
public ImageWithPoints SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
SelectedPointIndex = 1;
OnPropertyChanged();
OnPropertyChanged("Picture");
UpdatePoints();
}
}
public List<ImageWithPoints> Results
{
get
{
return _results;
}
set
{
_results = value;
if (value == null)
{
SelectedPointIndex = 0;
}
OnPropertyChanged();
}
}
public BitmapSource Picture
{
get
{
return SelectedItem?.Picture;
}
}
#endregion
#region Load
private ICommand _loadCommand;
public ICommand LoadCommand
{
get
{
if (_loadCommand == null)
_loadCommand = new RelayCommand(OnLoad, CanLoad);
return _loadCommand;
}
}
public void OnLoad()
{
StartRunning(this, null);
Task loadTask = new Task(new Action(() =>
{
Load();
Application.Current.Dispatcher.Invoke(new Action(() =>
{
StopRunning(this, null);
}));
}));
loadTask.Start();
}
public bool CanLoad()
{
return !IsRunning;
}
#endregion
#region Events
public event EventHandler OnStartRunning;
public event EventHandler OnStopRunning;
private void StartRunning(object sender, EventArgs e)
{
OnStartRunning(sender, e);
}
private void StopRunning(object sender, EventArgs e)
{
OnStopRunning(sender, e);
}
#enregion
#region Methods
public void Load()
{
// Open File
// Set to list
List<ImageWithPoints> listRes;
Results = listRes;
SelectedItem = Results[0];
}
#endregion
When I remove the line SelectedItem = Results[0]; I have no error (but application don't work has it should).
Set the SelectedItem property back on the UI thread once the Task has finished:
public void OnLoad()
{
StartRunning(this, null);
Task.Factory.StartNew(new Action(() =>
{
Load();
})).ContinueWith(task =>
{
SelectedItem = Results[0];
StopRunning(this, null);
}, System.Threading.CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
You can only access a UI element on the thread on which it was originally created so if your UpdatePoints() method accesses any control you must call this method on the UI thread.

WinRT 8.1 Asynchronous DataBinding not updating UI

My question is similar to some others that I have read, but I have not been able to find an answer to my specific issue.
Note:
I have read the following questions before asking:
ListView Data Binding for Windows 8.1 Store Apps
WinRT ViewModel DataBind to async method
That being said, I am creating a Windows 8.1 application that loads a text file asynchronously, and binds the data to a ListBox. I am sure that the issue has something to do with non-UI threads are not able to update the UI, so even though my data source implements INotifyPropertyChanged, the UI is not being updated when the data is loaded. Here is my LoadPayees() method:
public async void LoadPayees()
{
try
{
var json = await FileService.ReadFromFile("folder", "payees.json");
IList<Payee> payeesFromJson = JsonConvert.DeserializeObject<List<Payee>>(json);
var payees = new ObservableCollection<Payee>(payeesFromJson);
_payees = payees;
}
catch (Exception)
{
throw;
}
if (_payees == null)
{
_payees = new ObservableCollection<Payee>();
}
}
LoadPayees() is called in the OnNavigatedTo() event of my page. I can see via breakpoints that the method is being called, and the payees are being loaded into an ObservableCollection<Payee>. _payees is a property, which calls OnPropertyChanged() when it is set.
My question is, is there a way to have the UI thread be updated after LoadPayees() is finished loading the data? I also read somewhere that using a Task is no good for the UI as well. My static method FileService.ReadFromFile() returns a Task<string>.
Edit:
Here is my method ReadFromFile() which also calls to OpenFile():
public static async Task<string> ReadFromFile(string subFolderName, string fileName)
{
SetupFolder();
var file = await OpenFile(subFolderName, fileName);
var fileContents = string.Empty;
if (file != null)
{
fileContents = await FileIO.ReadTextAsync(file);
}
return fileContents;
}
public static async Task<StorageFile> OpenFile(string subFolderName, string fileName)
{
if (_currentFolder != null)
{
var folder = await _currentFolder.CreateFolderAsync(subFolderName, CreationCollisionOption.OpenIfExists);
return await folder.GetFileAsync(fileName);
}
else
{
return null;
}
}
Edit 2:
Here is the code for the properties, View, and OnNavigatedTo() as requested.
-- properties of the ViewModel --
private ObservableCollection<Payee> _payees;
private Payee _currentPayee;
public PayeesViewModel()
{
_currentPayee = new Payee();
_payees = new ObservableCollection<Payee>();
}
public ObservableCollection<Payee> Payees
{
get { return _payees; }
set
{
_payees = value;
OnPropertyChanged();
}
}
public Payee CurrentPayee
{
get { return _currentPayee; }
set
{
_currentPayee = value;
OnPropertyChanged();
}
}
-- view --
<StackPanel Orientation="Horizontal"
DataContext="{Binding Path=CurrentPayee}"
Grid.Row="1">
<Grid>
<!-- snip unrelated Grid code -->
</Grid>
<ListBox x:Name="PayeesListBox"
Margin="50,0,50,0"
Width="300"
ItemsSource="{Binding Path=Payees}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=CompanyName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
-- code behind --
private PayeesViewModel _vm = new PayeesViewModel();
public PayeesPage()
{
this.InitializeComponent();
this._navigationHelper = new NavigationHelper(this);
this._navigationHelper.LoadState += navigationHelper_LoadState;
this._navigationHelper.SaveState += navigationHelper_SaveState;
DataContext = _vm;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
_navigationHelper.OnNavigatedTo(e);
_vm.LoadPayees();
}
I think the problem is that you set the DataContext twice.
<StackPanel Orientation="Horizontal"
DataContext="{Binding Path=CurrentPayee}"
Grid.Row="1">
and
DataContext = _vm;
ListBox is a child from the outer StackPanel with DataContext CurrentPayee. On CurrentPayee you don't have Payees. You should not set DataContext multiple times.
Btw. change your code like following:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
_navigationHelper.OnNavigatedTo(e);
await _vm.LoadPayees();
}
public async Task LoadPayees()
{
try
{
var json = await FileService.ReadFromFile("folder", "payees.json");
IList<Payee> payeesFromJson = JsonConvert.DeserializeObject<List<Payee>>(json);
var payees = new ObservableCollection<Payee>(payeesFromJson);
_payees = payees;
}
catch (Exception)
{
throw;
}
if (_payees == null)
{
_payees = new ObservableCollection<Payee>();
}
}
You should never write async void for methods except event handlers.
Edit:
Change the ObservableCollection in your ViewModel. You should not have a public setter for a list.
private readonly ObservableCollection<Payee> _payees = new ObservableCollection<Payee>();
public ObservableCollection<Payee> Payees
{
get { return _payees; }
}
Than loop and add the items to the collection. Now, the view is notified.
foreach (var item in payeesFromJson)
{
Payees.Add(item);
}

Why List Box items are not update during Filtering?

I`m writing a program to view product Information in a Listbox. I have a text box for searching that automatically filters the list as you typing by ProductName. I have run through my C# code numerous times and I can see the filter actually working but I cannot visually get it to filter or 'refresh' on the screen.
C# Code :
private ICollectionView _ProductInfoView;
public ICollectionView ProductInfoView
{
get{return this._ProductInfoView;}
set
{
this._ProductInfoView=value;
this.onPropertyChnage("ProductInfoView");
}
}
private void RibbonSetupProduct_Click(object sender, System.Windows.RoutedEventArgs e)
{
this.hidePanels();
new Task(() =>
{
this.Dispatcher.Invoke(new Action(() =>
{
ObservableCollection<ModelProductInformation> productInfoCollection = new ObservableCollection<ModelProductInformation>(from ProductInfo in new GTS_ERPEntities().ProductInformations select new ModelProductInformation { ProductID = ProductInfo.ProductID, ProductName = ProductInfo.ProductName , Remark=ProductInfo.Remark});
this.ProductInfoView = CollectionViewSource.GetDefaultView(productInfoCollection);
new ProductInfoSearch(ProductInfoView, this.TestTextBox);
}
), DispatcherPriority.DataBind);
}
).Start();
this.PanelProducts.Visibility = Visibility.Visible;
}
class ProductInfoSearch
{
public ProductInfoSearch(ICollectionView filteredList, TextBox textEdit)
{
string filterText = string.Empty;
filteredList.Filter = delegate(object obj)
{
if (String.IsNullOrEmpty(filterText))
{
return true;
}
ModelProductInformation str = obj as ModelProductInformation;
if (str.ProductName==null)
{
return true;
}
if (str.ProductName.ToUpper().Contains(filterText.ToUpper()))
{
return true;
}
else
{
return false;
}
};
textEdit.TextChanged += delegate
{
filterText = textEdit.Text;
filteredList.Refresh();
};
}
}
XAML :
<dxe:ListBoxEdit x:Name="ProductInfoList" Margin="1.666,1,8,8" Grid.Column="2" Grid.Row="2" Grid.RowSpan="5" DisplayMember="ProductName" DataContext="{Binding ProductInfoView, ElementName=window}" ItemsSource="{Binding}"/>
I guess my problem is either Data Binding or inside Task().
i would make the ObservableCollection a private field and just create the instance once and also just create the ICollectionView once. to add new data you can use clear and add on your Collection - try it, it works for me.
private ObservableCollection<ModelProductInformation> productInfoCollection;
//ctor, just once in the constructor
this.productInfoCollection = new ObservableCollection<ModelProductInformation>();
this.ProductInfoView = CollectionViewSource.GetDefaultView(productInfoCollection);
new ProductInfoSearch(ProductInfoView, this.TestTextBox);
private void RibbonSetupProduct_Click(object sender, System.Windows.RoutedEventArgs e)
{
this.hidePanels();
new Task(() =>
{
this.Dispatcher.Invoke(new Action(() =>
{
var yourData = from ProductInfo in new GTS_ERPEntities().ProductInformations select new ModelProductInformation { ProductID = ProductInfo.ProductID, ProductName = ProductInfo.ProductName , Remark=ProductInfo.Remark};
//if you wanna change the collection, simple clear and add(or create AddRange extension)
this.productInfoCollection.Clear();
foreach(var data in yourData)
{ this.productInfoCollection.Add(data);}
}
), DispatcherPriority.DataBind);
}
).Start();
this.PanelProducts.Visibility = Visibility.Visible;
}

Editing newly added row in Silverlight 3 DataGrid using MVVM

I am trying to use the Silverlight 3.0 DataGrid with the MVVM design pattern. My page has a DataGrid and a button that adds an item to the collection in the VM using a command (from the Composite Application Library). This works fine, and the new item is displayed and selected.
The problem I can't solve is how to begin editing the row. I want the new row to be immediately editable when the user clicks the Add button i.e. focus set to the DataGrid and the new row in edit mode.
This is the XAML in the view:
<Grid x:Name="LayoutRoot">
<StackPanel>
<data:DataGrid ItemsSource="{Binding DataView}"/>
<Button cmd:Click.Command="{Binding AddItemCommand}" Content="Add" />
</StackPanel>
</Grid>
The code behind has one line of code that creates an instance of the VM and sets the DataContext of the view.
The VM code is:
public class VM
{
public List<TestData> UnderlyingData { get; set; }
public PagedCollectionView DataView { get; set; }
public ICommand AddItemCommand { get; set; }
public VM()
{
AddItemCommand = new DelegateCommand<object>(o =>
{
DataView.AddNew();
});
UnderlyingData = new List<TestData>();
UnderlyingData.Add(new TestData() { Value = "Test" });
DataView = new PagedCollectionView(UnderlyingData);
}
}
public class TestData
{
public string Value { get; set; }
public TestData()
{
Value = "<new>";
}
public override string ToString()
{
return Value.ToString();
}
}
What would be the best way to solve this problem using the MVVM design pattern?
I faced the same issue. I've introduced interface ISupportEditingState:
public interface ISupportEditingState
{
EditingState EditingState { get; set; }
}
My VM implements it. And then I wrote this behaviour to synchronise editing state of DataGrid and my VM:
public class SynchroniseDataGridEditingStateBehaviour : Behavior<DataGrid>
{
public static readonly DependencyProperty EditingStateBindingProperty =
DependencyProperty.Register("EditingStateBinding", typeof(ISupportEditingState),
typeof(SynchroniseDataGridEditingStateBehaviour), new PropertyMetadata(OnEditingStateBindingPropertyChange));
private bool _attached;
private bool _changingEditingState;
public ISupportEditingState EditingStateBinding
{
get { return (ISupportEditingState)GetValue(EditingStateBindingProperty); }
set { SetValue(EditingStateBindingProperty, value); }
}
private static void OnEditingStateBindingPropertyChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as SynchroniseDataGridEditingStateBehaviour;
if (b == null)
return;
var oldNotifyChanged = e.OldValue as INotifyPropertyChanged;
if (oldNotifyChanged != null)
oldNotifyChanged.PropertyChanged -= b.OnEditingStatePropertyChanged;
var newNotifyChanged = e.NewValue as INotifyPropertyChanged;
if (newNotifyChanged != null)
newNotifyChanged.PropertyChanged += b.OnEditingStatePropertyChanged;
var newEditingStateSource = e.NewValue as ISupportEditingState;
if (newEditingStateSource.EditingState == EditingState.Editing)
{
// todo: mh: decide on this behaviour once again.
// maybe it's better to start editing if selected item is already bound in the DataGrid
newEditingStateSource.EditingState = EditingState.LastCancelled;
}
}
private static readonly string EditingStatePropertyName =
CodeUtils.GetPropertyNameByLambda<ISupportEditingState>(ses => ses.EditingState);
private void OnEditingStatePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_changingEditingState || !_attached || e.PropertyName != EditingStatePropertyName)
return;
_changingEditingState = true;
var editingStateSource = sender as ISupportEditingState;
if (editingStateSource == null)
return;
var grid = AssociatedObject;
var editingState = editingStateSource.EditingState;
switch (editingState)
{
case EditingState.Editing:
grid.BeginEdit();
break;
case EditingState.LastCancelled:
grid.CancelEdit();
break;
case EditingState.LastCommitted:
grid.CommitEdit();
break;
default:
throw new InvalidOperationException("Provided EditingState is not supported by the behaviour.");
}
_changingEditingState = false;
}
protected override void OnAttached()
{
var grid = AssociatedObject;
grid.BeginningEdit += OnBeginningEdit;
grid.RowEditEnded += OnEditEnded;
_attached = true;
}
protected override void OnDetaching()
{
var grid = AssociatedObject;
grid.BeginningEdit -= OnBeginningEdit;
grid.RowEditEnded -= OnEditEnded;
_attached = false;
}
void OnEditEnded(object sender, DataGridRowEditEndedEventArgs e)
{
if (_changingEditingState)
return;
EditingState editingState;
if (e.EditAction == DataGridEditAction.Commit)
editingState = EditingState.LastCommitted;
else if (e.EditAction == DataGridEditAction.Cancel)
editingState = EditingState.LastCancelled;
else
return; // if DataGridEditAction will ever be extended, this part must be changed
EditingStateBinding.EditingState = editingState;
}
void OnBeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (_changingEditingState)
return;
EditingStateBinding.EditingState = EditingState.Editing;
}
}
Works ok for me, hope it helps.
Whenever you talk about directly accessing ui components, your kinda missing the point of mvvm. The ui binds to the viewmodel, so find a way to alter the viewmodel instead.

Categories

Resources