I have an large application with 30 plus projects that currently uses an IRepository and POCO entities. I would like to know if there is a way to use table storage without having to implement ITableEntity. I don't want to have to import the azure storage nugget packages into every project and change all my entities to use ITableEntity.
Entity Adapater
I am aware that it is possible to create an entity adapter (such as that below) which works quite well when reading or writing an individual entity. But I have not been able to get this to work when attempting to expose IQueryable via table.CreateQuery().
public class AzureEntity
{
public Guid Id { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string ETag { get; set; }
}
internal class AzureStorageEntityAdapter<T> : ITableEntity where T : AzureEntity, new()
{
#region Properties
/// <summary>
/// Gets or sets the entity's partition key
/// </summary>
public string PartitionKey
{
get { return InnerObject.PartitionKey; }
set { InnerObject.PartitionKey = value; }
}
/// <summary>
/// Gets or sets the entity's row key.
/// </summary>
public string RowKey
{
get { return InnerObject.RowKey; }
set { InnerObject.RowKey = value; }
}
/// <summary>
/// Gets or sets the entity's Timestamp.
/// </summary>
public DateTimeOffset Timestamp
{
get { return InnerObject.Timestamp; }
set { InnerObject.Timestamp = value; }
}
/// <summary>
/// Gets or sets the entity's current ETag.
/// Set this value to '*' in order to blindly overwrite an entity as part of an update operation.
/// </summary>
public string ETag
{
get { return InnerObject.ETag; }
set { InnerObject.ETag = value; }
}
/// <summary>
/// Place holder for the original entity
/// </summary>
public T InnerObject { get; set; }
#endregion
#region Ctor
public AzureStorageEntityAdapter()
{
// If you would like to work with objects that do not have a default Ctor you can use (T)Activator.CreateInstance(typeof(T));
this.InnerObject = new T();
}
public AzureStorageEntityAdapter(T innerObject)
{
this.InnerObject = innerObject;
}
#endregion
#region Methods
public virtual void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext)
{
TableEntity.ReadUserObject(this.InnerObject, properties, operationContext);
}
public virtual IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
{
return TableEntity.WriteUserObject(this.InnerObject, operationContext);
}
#endregion
}
I would like to be able to do something like this...
public class TableStorageRepository : IRepository
{
// snip...
public IQueryable<T> FindAll<T>() where T : class, new()
{
CloudTable table = GetCloudTable<T>();
return table.CreateQuery<AzureStorageEntityAdapter<T>>();
}
// snip...
}
The problem here is the CreateQuery creates an
IQueryable<AzureStorageEntityApater<T>>.
I can't see how to get an IQueryable of all the 'InnerObjects'.
Does anybody know if it is possible to expose IQueryable by some means without exposting ITableEntity?
This may or may not be what you want, but might give you some ideas.
I created a base repository and used a separate repository for each entity which is responsible for passing the correct CloudTable, partition/rowkey/property expressions and resolvers in. I base it around DynamicTableEntity (which allows for some advanced stuff like dynamic properties in my entities (like small collections)).
public class BaseRepository
{
protected async Task<DynamicTableEntity> GetAsync(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter)
{
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).AsTableQuery();
var segment = await query.ExecuteSegmentedAsync(null);
return segment.Results.FirstOrDefault();
}
protected async Task<T> GetAsync<T>(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, EntityResolver<T> resolver)
{
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Resolve(resolver);
var segment = await query.ExecuteSegmentedAsync(null);
return segment.Results.FirstOrDefault();
}
protected async Task<IEnumerable<DynamicTableEntity>> GetAllAsync(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, int take = 1000)
{
if (take > 10000) take = 10000;
if (take < 1) take = 1;
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Take(take).AsTableQuery();
var token = new TableContinuationToken();
var results = new List<DynamicTableEntity>();
while (token != null)
{
var segment = await query.ExecuteSegmentedAsync(token);
results.AddRange(segment.Results);
token = segment.ContinuationToken;
}
return results;
}
protected async Task<IEnumerable<T>> GetAllAsync<T>(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, EntityResolver<T> resolver, int take = 1000)
{
if (take > 10000) take = 10000;
if (take < 1) take = 1;
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Take(take).Resolve(resolver);
var token = new TableContinuationToken();
var results = new List<T>();
while (token != null)
{
var segment = await query.ExecuteSegmentedAsync(token);
results.AddRange(segment.Results);
token = segment.ContinuationToken;
}
return results;
}
protected async Task<int> InsertAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Insert(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> ReplaceAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Replace(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> DeleteAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Delete(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> MergeAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Merge(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
}
Example of a class inheriting from it (you'll have to use your imagination to fill in the blanks - let me know if you want to see a proper implementation)
// method
public Task<IEnumerable<T>> GetAllAsync<T>(string pk1, string pk2, EntityResolver<T> resolver, int take = 1000, Expression<Func<DynamicTableEntity, bool>> filterExpr = null)
{
var keysExpr = x => x.PartitionKey.Equals(string.Format("{0}_{1}", pk1, pk2);
var queryExpr = filterExpr != null ? keysExpr.AndAlso(filterExpr) : keysExpr;
return base.GetAllAsync<T>(CloudTableSelector.GetTable(), queryExpr, resolver, take);
}
// call
var products = await ProductRepo.GetAllAsync<ProductOwnerViewDto>(orgType, orgId, ProductOwnerViewDto.GetResolver(), take, x => x.RowKey.CompareTo(fromId) > 0);
It's a bit raw and a pain to wrap all your entities in separate repos, but I couldn't find a way to let me query the table in such a manner whilst letting me get different projections out (multiple resolvers per table).
I find solutions based on ITableEntity limiting (which is fine until you need dynamic properties then you're screwed).
Related
I have SharePoint List which content a Reference No. It'd URL look like this:
https://xyz.sharepoint.com/sites/site_name/Lists/List_name/AllItems.aspx
This List content ref no. I am trying to insert this data in the list.
{
"Optimum_x0020_Case_x0020_Reference": "000777"
}
This is url I am posting the data.
https://graph.microsoft.com/v1.0/sites/xyz.sharepoint.com:/sites/site_name:/lists/List_names/items
But I am getting this error:
error": {
"code": "accessDenied",
"message": "The caller does not have permission to perform the action.",
How to solve this? Using the access I am able to create folder, sub folder and Update meta data for other document.
What is the context of what you are doing this with? Is it an app that you are using? Are you inserting data on a already existing listitem or a new item?
This is the code I had to use for my UWP App. I'm not sure if this will help you or not, but it should give you a little guidance I hope. Creating the dictionary and figuring out the XML structure were the keys things I had to piece together to get my code to work.
I declared my scopes in my App.xaml.cs
public static string[] scopes = new string[] { "user.ReadWrite", "Sites.ReadWrite.All", "Files.ReadWrite.All" };
I have a submit button that I use on my MainPage
private async void SubmitButton_Click(object sender, RoutedEventArgs e)
{
var (authResult, message) = await Authentication.AquireTokenAsync();
if (authResult != null)
{
await SubmitDataWithTokenAsync(submiturl, authResult.AccessToken);
}
}
This calls the AquireToken which I have in a class file:
public static async Task<(AuthenticationResult authResult, string message)> AquireTokenAsync()
{
string message = String.Empty;
string[] scopes = App.scopes;
AuthenticationResult authResult = null;
message = string.Empty;
//TokenInfoText.Text = string.Empty;
IEnumerable<IAccount> accounts = await App.PublicClientApp.GetAccountsAsync();
IAccount firstAccount = accounts.FirstOrDefault();
try
{
authResult = await App.PublicClientApp.AcquireTokenSilentAsync(scopes, firstAccount);
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await App.PublicClientApp.AcquireTokenAsync(scopes);
}
catch (MsalException msalex)
{
message = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
message = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
}
return (authResult,message);
}
I had created another class for my SharePointList
public class SharePointListItems
{
public class Lookup
{
public string SerialNumber { get; set; }
public string id { get; set; }
public override string ToString()
{
return SerialNumber;
}
}
public class Value
{
public Lookup fields { get; set; }
}
public class Fields
{
[JsonProperty("#odata.etag")]
public string ODataETag { get; set; }
public string ParameterA { get; set; }
public string ParameterB { get; set; }
public string ParameterC { get; set; }
}
public class RootObject
{
[JsonProperty("#odata.context")]
public string ODataContext { get; set; }
[JsonProperty("#odata.etag")]
public string ODataETag { get; set; }
[JsonProperty("fields#odata.context")]
public string FieldsODataContext { get; set; }
public Fields Fields { get; set; }
}
}
I used this class to create a dictionary for submitting my data to SharePoint.
public async Task<string> SubmitDataWithTokenAsync(string url, string token)
{
var httpClient = new HttpClient();
HttpResponseMessage response;
try
{
var root = new
{
fields = new Dictionary<string, string>
{
// The second string are public static strings that I
// I declared in my App.xaml.cs because of the way my app
// is set up.
{ "ParameterA", App.ParameterA },
{ "ParameterB", App.ParameterB },
{ "ParameterC", App.ParameterC },
}
};
var s = new JsonSerializerSettings { DateFormatHandling = DateFormatHandling.MicrosoftDateFormat };
var content = JsonConvert.SerializeObject(root, s);
var request = new HttpRequestMessage(HttpMethod.Post, url);
//Add the token in Authorization header
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
response = await httpClient.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
catch (Exception ex)
{
return ex.ToString();
}
}
And my submiturl is defined:
public static string rooturl = "https://graph.microsoft.com/v1.0/sites/xxxxxx.sharepoint.com,495435b4-60c3-49b7-8f6e-1d262a120ae5,0fad9f67-35a8-4c0b-892e-113084058c0a/";
string submiturl = rooturl + "lists/18a725ac-83ef-48fb-a5cb-950ca2378fd0/items";
You can also look at my posted question on a similar topic here.
My unit of work class is mentioned below and I am using Ninject and I have tried injecting IUnitOfWork per request per thread scope, transient etc. but I am still getting error which is:
"Message":"An error has occurred.","ExceptionMessage":"The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.","ExceptionType":"System.InvalidOperationException
I get this error when i make two web API (get) calls at the same time using angularJS and it shows error at the point _context.Set<TEntity>().FirstOrDefault(match);
public class UnitOfWork : IUnitOfWork, IDisposable
{
private My_PromotoolEntities _uowDbContext = new My_PromotoolEntities();
private Dictionary<string, object> _repositories;
// Do it like this if no specific class file
private GenericRepository<MysPerson> _personRepository;
//private GenericRepository<MysDataSource> dataSourcesRepository;
//private GenericRepository<MysCountry> countryMasterRepository;
// Or like this if with specific class file.
private DataSourceRepository _dataSourcesRepository;
private CustomerRepository _customerRepository;
private DeviceRepository _deviceRepository;
private DeviceRegistrationRepository _deviceRegistrationRepository;
private EmailQueueRepository _emailQueueRepository;
public void SetContext(My_PromotoolEntities context)
{
_uowDbContext = context;
}
public void CacheThis(object cacheThis, string keyName, TimeSpan howLong)
{
Cacheing.StaticData.CacheStaticData(cacheThis, keyName, howLong);
}
public object GetFromCache(string keyName)
{
return Cacheing.StaticData.GetFromCache(keyName);
}
public GenericRepository<T> GenericRepository<T>() where T : BaseEntity
{
if (_repositories == null)
{
_repositories = new Dictionary<string, object>();
}
var type = typeof(T).Name;
if (!_repositories.ContainsKey(type))
{
var repositoryType = typeof(GenericRepository<>);
var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), _uowDbContext);
_repositories.Add(type, repositoryInstance);
}
return (GenericRepository<T>)_repositories[type];
}
public GenericRepository<MysPerson> PersonRepository
{
get
{
if (this._personRepository == null)
{
this._personRepository = new GenericRepository<MysPerson>(_uowDbContext);
}
return _personRepository;
}
}
public DataSourceRepository DataSourcesRepository
{
get
{
if (this._dataSourcesRepository == null)
{
this._dataSourcesRepository = new DataSourceRepository(_uowDbContext);
}
return _dataSourcesRepository;
}
}
public CustomerRepository CustomerRepository
{
get
{
if (this._customerRepository == null)
{
this._customerRepository = new CustomerRepository(_uowDbContext);
}
return _customerRepository;
}
}
public DeviceRepository DeviceRepository
{
get
{
if (this._deviceRepository == null)
{
this._deviceRepository = new DeviceRepository(_uowDbContext);
}
return _deviceRepository;
}
}
public DeviceRegistrationRepository DeviceRegistrationRepository
{
get
{
if (this._deviceRegistrationRepository == null)
{
this._deviceRegistrationRepository = new DeviceRegistrationRepository(_uowDbContext);
}
return _deviceRegistrationRepository;
}
}
public EmailQueueRepository emailQueueRepository
{
get
{
if (this._emailQueueRepository == null)
{
this._emailQueueRepository = new EmailQueueRepository(_uowDbContext);
}
return _emailQueueRepository;
}
}
/// <summary>
/// Commits all changes to the db. Throws exception if fails. Call should be in a try..catch.
/// </summary>
public void Save()
{
try
{
_uowDbContext.SaveChanges();
}
catch (DbEntityValidationException dbevex)
{
// Entity Framework specific errors:
StringBuilder sb = new StringBuilder();
var eve = GetValidationErrors();
if (eve.Count() > 0)
{
eve.ForEach(error => sb.AppendLine(error));
}
ClearContext();
// Throw a new exception with original as inner.
var ex = new Exception(sb.ToString(), dbevex);
ex.Source = "DbEntityValidationException";
throw ex;
}
catch (Exception)
{
ClearContext();
throw;
}
}
private void ClearContext()
{
DetachAll();
}
private void DetachAll()
{
foreach (DbEntityEntry dbEntityEntry in _uowDbContext.ChangeTracker.Entries())
{
if (dbEntityEntry.Entity != null)
{
dbEntityEntry.State = EntityState.Detached;
}
}
}
/// <summary>
/// Checks for EF DbEntityValidationException(s).
/// </summary>
/// <returns>Returns a List of string containing the EF DbEntityValidationException(s).</returns>
public List<string> GetValidationErrors()
{
if (_uowDbContext.GetValidationErrors().Count() != 0)
{
return _uowDbContext.GetValidationErrors().Select(e => string.Join(Environment.NewLine, e.ValidationErrors.Select(v => string.Format("{0} - {1}", v.PropertyName, v.ErrorMessage)))).ToList();
}
return null;
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
_uowDbContext.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
You should never use a context in 2 places at the same time, that's exactly why you are getting this error. From the MSDN documentation:
Thread Safety: Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
It is a little hard to make suggestions without a repro but there is a brute force approach that should resolve the issue. If you have an interception point before/during DI setup then you can cause all the context initialization etc to happen by creating an instance of your context and calling ctx.Database.Initialize(force: false); Passing 'force: false' will ensure that the initialization still only happens once per AppDomain
When attempting to load an instantiated export with GetExports() (using a LINQ query described below), the method returns null. I notice that when I call GetExports without the LINQ query, the return value is Count: 0. This would indicate to me that MEF is failing to find any exports that have been composed in the container. I can see the ExportDefinition, however, when looking at Container.Catalog.Parts.ExportDefinitions. Any ideas on where I am going wrong? Everything up until the query seems to be working fine.
I have the following contract and metadata view declared and implemented:
public interface IMap
{
void Init();
int ParseData();
}
public interface IMapMetadata
{
string MapName { get; }
string DocumentType { get; }
}
[Export(typeof(IMap))]
[ExportMetadata("MapName", "Map")]
public class Map
{
public Map()
{
}
}
I am using the following code to load a directory that contains DLLs that satisfy this contract with:
public void LoadByDirectory(string zPath)
{
try
{
_catalog.Catalogs.Add(new DirectoryCatalog(zPath));
}
catch (Exception e)
{
String zErrMess = e.Message;
}
}
Using a LINQ query to get an export:
public IMap GetMapInstance(string zMapName)
{
IMap ndeMap;
_container = new CompositionContainer(_catalog);
_container.ComposeParts(this);
try
{
ndeMap = _container.GetExports<IMap, IMapMetadata>()
.Where(p => p.Metadata.MapName.Equals(zMapName))
.Select(p => p.Value)
.FirstOrDefault();
}
catch (Exception ex)
{
throw new Exception("Failed to load map " + zMapName + ": " + ex.Message, ex);
}
return ndeMap;
}
Calling the above method like this:
IMap map = mapFactory.GetMapInstance("Map");
returns null.
UPDATED
In addition to the answer below, I was forgetting to declare the interface on the map class, this resolves the issue (note I removed the DocumentType property):
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class MapExportAttribute : ExportAttribute, IMapMetadata
{
public MapExportAttribute()
: base(typeof(IMap))
{
}
public string MapName { get; set; }
}
[MapExport(MapName="Map")]
public class Map : IMap
{
public Map()
{
}
public void Init()
{
throw new NotImplementedException();
}
public int ParseData()
{
throw new NotImplementedException();
}
}
It looks like you're missing the DocumentType meta-data on your export:
[Export(typeof(IMap))]
[ExportMetadata("MapName", "Map")]
[ExportMetadata("DocumentType", "???")]
public class Map
{
}
The simplest way to ensure you specify the correct meta-data is a custom export attribute:
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class MapExportAttribute : ExportAttribute, IMapMetadata
{
public MapExportAttribute() : base(typeof(IMap))
{
}
public string MapName { get; set; }
public string DocumentType { get; set; }
}
[MapExport(MapName = "Map")]
public class Map
{
}
For a plugin (like the extension library) I try to access the datasource with a given "var" name. Accessing the Datasource object is very easy with the following code:
m_DataSourceName contains the name (var) of the datasource.
public DataSource getDataSource() {
if (StringUtil.isNotEmpty(m_DataSourceName)) {
UIViewRoot vrCurrent = getFacesContext().getViewRoot();
if (vrCurrent instanceof UIViewRootEx) {
for (DataSource dsCurrent : ((UIViewRootEx) vrCurrent)
.getData()) {
if (m_DataSourceName.equals(dsCurrent.getVar())) {
return dsCurrent;
}
}
}
}
System.out.println("Datasource name:" + m_DataSourceName);
return null;
}
I'm getting the datasource back and I can cast this datasource:
private TabularDataModel getTDM(DataSource dsCurrent, FacesContext context) {
try {
if (dsCurrent instanceof ModelDataSource) {
ModelDataSource mds = (ModelDataSource) dsCurrent;
AbstractDataSource ads = (AbstractDataSource) mds;
ads.load(context);
System.out.println(ads.getBeanId());
if (ads.getBeanId() == null) {
}
DataModel tdm = mds.getDataModel();
if (tdm instanceof TabularDataModel) {
TabularDataModel tds = (TabularDataModel) tdm;
return tds;
}
}
return null;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
And now I wanna access the TDM.getRowCount() and this point I'm getting a nullpointer exception. The datasource contains a notes view. Did I miss anything to initialize the datasource?
Here is a solution for your problem:
This will give you all lines of a view, not the entry count*. F.e. if you have a categorized view with 5 categories and 1 entry for each category, this will result in 10 lines. The entry count is 5.
First, create a dummy class which implements FacesDataIterator
public class DummyDataIterator implements com.ibm.xsp.component.FacesDataIterator{
public DataModel getDataModel() {
return null;
}
public int getFirst() {
return 0;
}
public int getRowIndex() {
return 0;
}
public int getRows() {
return 0;
}
public void setFirst(int paramInt) {}
public void setRows(int paramInt) {}
}
And then you have to do the following:
Set the data iterator
tdm.setDataControl( new DummyDataIterator() );
Init the row counter for the first time
tdm.getRowCount();
Calculate the exact row count with a navigator
(( com.ibm.xsp.model.domino.viewnavigator.NOIViewNavigatorEx) tdm.getDominoViewDataContainer().getNavigator()).calculateExactCount(tdm.getView());
Now your row count is initialized, you can get the result with a normal getRowCount:
System.out.println("Rows: " + tdm.getRowCount() );
Hope this helps!
*:
tdm.getView().getAllEntries().getCount()
I'm trying to find a clean way to accomplish MvvmLight's IDataService pattern with async/await.
Originally I was using the callback Action method to work similar to the template's but that doesn't update the UI either.
public interface IDataService
{
void GetData(Action<DataItem, Exception> callback);
void GetLocationAsync(Action<Geoposition, Exception> callback);
}
public class DataService : IDataService
{
public void GetData(Action<DataItem, Exception> callback)
{
// Use this to connect to the actual data service
var item = new DataItem("Location App");
callback(item, null);
}
public async void GetLocationAsync(Action<Geoposition, Exception> callback)
{
Windows.Devices.Geolocation.Geolocator locator = new Windows.Devices.Geolocation.Geolocator();
var location = await locator.GetGeopositionAsync();
callback(location, null);
}
}
public class MainViewModel : ViewModelBase
{
private readonly IDataService _dataService;
private string _locationString = string.Empty;
public string LocationString
{
get
{
return _locationString;
}
set
{
if (_locationString == value)
{
return;
}
_locationString = value;
RaisePropertyChanged(LocationString);
}
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
_dataService.GetLocation(
(location, error) =>
{
LocationString = string.Format("({0}, {1})",
location.Coordinate.Latitude,
location.Coordinate.Longitude);
});
}
}
I'm trying to databind against gps coordinates but since the async fires and doesn't run on main thread it doesn't update the UI.
Might be unrelated, but AFAICT you're missing some quotes
RaisePropertyChanged(LocationString);
You pass the name of the property that changed, not its value.