Skip to content

feat: Update documentation for .NET 9 and MAUI #957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: gh-pages
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _includes/cloudcode/cloud-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ $ratings = ParseCloud::run("averageRatings", ["movie" => "The Matrix"]);
// $ratings is 4.5
```

The following example shows how you can call the "averageRatings" Cloud function from a .NET C# app such as in the case of Windows 10, Unity, and Xamarin applications:
The following example shows how you can call the "averageRatings" Cloud function from a .NET C# app such as in the case of Windows 10, Unity, and Xamarin/.NET MAUI applications:

```cs
IDictionary<string, object> params = new Dictionary<string, object>
Expand Down
121 changes: 92 additions & 29 deletions _includes/dotnet/analytics.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,108 @@
# Analytics

Parse provides a number of hooks for you to get a glimpse into the ticking heart of your app. We understand that it's important to understand what your app is doing, how frequently, and when.
Parse provides tools to gain insights into your app's activity. You can track app launches, custom events, and more. These analytics are available even if you primarily use Parse for data storage. Your app's dashboard provides real-time graphs and breakdowns (by device type, class name, or REST verb) of API requests, and you can save graph filters.

While this section will cover different ways to instrument your app to best take advantage of Parse's analytics backend, developers using Parse to store and retrieve data can already take advantage of metrics on Parse.
## App-Open Analytics

Without having to implement any client-side logic, you can view real-time graphs and breakdowns (by device type, Parse class name, or REST verb) of your API Requests in your app's dashboard and save these graph filters to quickly access just the data you're interested in.
Track application launches by calling `TrackAppOpenedAsync()` in your app's launch event handler. This provides data on when and how often your app is opened. Since MAUI does not have a single, clear "launching" event like some other platforms, the best place to put this call is in your `App.xaml.cs` constructor, *after* initializing the Parse client:

## App-Open / Push Analytics
```csharp
// In App.xaml.cs
public App()
{
InitializeComponent();

Our initial analytics hook allows you to track your application being launched. By adding the following line to your Launching event handler, you'll be able to collect data on when and how often your application is opened.
MainPage = new AppShell();

```cs
ParseAnalytics.TrackAppOpenedAsync();
// Initialize Parse Client (See initialization documentation)
if (!InitializeParseClient())
{
// Handle initialization failure
Console.WriteLine("Failed to initialize Parse.");
}
else
{
// Track app open *after* successful Parse initialization.
Task.Run(() => ParseClient.Instance.TrackLaunchAsync()); // Do not await in the constructor.
}
}
```

**Important Considerations:**

* **`Task.Run()`:** We use `Task.Run()` to call `TrackLaunchAsync()` *without* awaiting it in the `App` constructor. This is crucial because the constructor should complete quickly to avoid delaying app startup. `TrackLaunchAsync` will run in the background. If Parse initialization fails, we *don't* track the app open.
* **MAUI Lifecycle:** MAUI's lifecycle events are different from older platforms. There isn't a single, universally appropriate "launching" event. The `App` constructor is generally a good place, *provided* you initialize Parse first and handle potential initialization failures. Other possible locations (depending on your specific needs) might include the `OnStart` method of your `App` class, or the first page's `OnAppearing` method. However, the constructor ensures it's tracked as early as possible.
* **Push Notifications:** If you are using Push Notifications, you'll likely need to handle tracking opens from push notifications separately, in the code that handles the push notification reception and user interaction. This is *not* covered in this basic analytics section (see the Push Notifications documentation).

## Custom Analytics

`ParseAnalytics` also allows you to track free-form events, with a handful of `string` keys and values. These extra dimensions allow segmentation of your custom events via your app's Dashboard.

Say your app offers search functionality for apartment listings, and you want to track how often the feature is used, with some additional metadata.

```cs
var dimensions = new Dictionary<string, string> {
// Define ranges to bucket data points into meaningful segments
{ "priceRange", "1000-1500" },
// Did the user filter the query?
{ "source", "craigslist" },
// Do searches happen more often on weekdays or weekends?
{ "dayType", "weekday" }
};
// Send the dimensions to Parse along with the 'search' event
ParseAnalytics.TrackEventAsync("search", dimensions);
Track custom events with `TrackAnalyticsEventAsync()`. You can include a dictionary of `string` key-value pairs (dimensions) to segment your events.

Example: Tracking apartment search usage:

```csharp
public async Task TrackSearchEventAsync(string priceRange, string source, string dayType)
{
var dimensions = new Dictionary<string, string>
{
{ "priceRange", priceRange },
{ "source", source },
{ "dayType", dayType }
};

try
{
await ParseClient.Instance.TrackAnalyticsEventAsync("search", dimensions);
}
catch (Exception ex)
{
// Handle errors (e.g., network issues)
Console.WriteLine($"Analytics tracking failed: {ex.Message}");
}
}

// Example usage:
await TrackSearchEventAsync("1000-1500", "craigslist", "weekday");

```

`ParseAnalytics` can even be used as a lightweight error tracker &mdash; simply invoke the following and you'll have access to an overview of the rate and frequency of errors, broken down by error code, in your application:
You can use `TrackAnalyticsEventAsync` for lightweight error tracking:

```csharp
// Example: Tracking errors
public async Task TrackErrorEventAsync(int errorCode)
{
var dimensions = new Dictionary<string, string>
{
{ "code", errorCode.ToString() }
};

try
{
await ParseClient.Instance.TrackAnalyticsEventAsync("error", dimensions);
}
catch (Exception ex)
{
//It failed.. not much to do, is there?
Console.WriteLine($"Analytics tracking failed: {ex.Message}");
}
}

```cs
var errDimensions = new Dictionary<string, string> {
{ "code", Convert.ToString(error.Code) }
};
ParseAnalytics.TrackEventAsync("error", errDimensions );
// Example usage (within a catch block):
catch (Exception ex)
{
// ... other error handling ...
await TrackErrorEventAsync(123); // Replace 123 with a meaningful error code.
}
```

Note that Parse currently only stores the first eight dimension pairs per call to `ParseAnalytics.TrackEventAsync()`.
**Limitations:**

* Parse stores only the first eight dimension pairs per `TrackAnalyticsEventAsync()` call.

**API Changes and Usage (Key Points):**

* **`ParseAnalytics.TrackAppOpenedAsync()` is now `ParseClient.Instance.TrackLaunchAsync()`:** The methods are now extension methods on the `IServiceHub` interface, and you access them via `ParseClient.Instance`.
* **`ParseAnalytics.TrackEventAsync()` is now `ParseClient.Instance.TrackAnalyticsEventAsync()`:** Similar to the above, this is now an extension method.
* **Asynchronous Operations:** All analytics methods are asynchronous (`async Task`). Use `await` when calling them (except in specific cases like the `App` constructor, where you should use `Task.Run()` to avoid blocking).
* **Error Handling** Use `try-catch` and handle `Exception`.
105 changes: 78 additions & 27 deletions _includes/dotnet/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,106 @@

## The ParseFile

`ParseFile` lets you store application files in the cloud that would otherwise be too large or cumbersome to fit into a regular `ParseObject`. The most common use case is storing images but you can also use it for documents, videos, music, and any other binary data.
`ParseFile` allows you to store large files (images, documents, videos, etc.) in the cloud, which would be impractical to store directly within a `ParseObject`.

Getting started with `ParseFile` is easy. First, you'll need to have the data in `byte[]` or `Stream` form and then create a `ParseFile` with it. In this example, we'll just use a string:
**Creating a `ParseFile`:**

```cs
You need the file data as a `byte[]` or a `Stream`. You also provide a filename (including the extension).

```csharp
// Example 1: From a byte array
byte[] data = System.Text.Encoding.UTF8.GetBytes("Working at Parse is great!");
ParseFile file = new ParseFile("resume.txt", data);

// Example 2: From a Stream (e.g., a file stream)
using (FileStream stream = File.OpenRead("path/to/your/file.png"))
{
ParseFile fileFromStream = new ParseFile("image.png", stream);
await fileFromStream.SaveAsync(); // Save immediately, or later.
}

// Example 3: From a Stream with a known content type (recommended)
using (FileStream stream = File.OpenRead("path/to/your/file.pdf"))
{
ParseFile fileFromStream = new ParseFile("document.pdf", stream, "application/pdf");
await fileFromStream.SaveAsync();
}
```

Notice in this example that we give the file a name of `resume.txt`. There's two things to note here:
**Important Notes:**

* You don't need to worry about filename collisions. Each upload gets a unique identifier so there's no problem with uploading multiple files named `resume.txt`.
* It's important that you give a name to the file that has a file extension. This lets Parse figure out the file type and handle it accordingly. So, if you're storing PNG images, make sure your filename ends with `.png`.
* **Unique Identifiers:** Parse handles filename collisions. Each uploaded file gets a unique identifier. You *can* upload multiple files with the same name.
* **File Extension:** The filename *must* include the correct file extension (e.g., `.txt`, `.png`, `.jpg`, `.pdf`). This allows Parse to determine the file type and handle it appropriately (e.g., serving images with the correct `Content-Type` header).
* **Content Type (Optional but Recommended):** When creating a `ParseFile` from a `Stream`, you can *optionally* provide the content type (MIME type) as a third argument (e.g., "image/png", "application/pdf", "text/plain"). This is *highly recommended* as it ensures the file is served correctly. If you don't provide it, Parse will try to infer it from the filename extension, but providing it explicitly is more reliable. If creating from a `byte[]`, content type is inferred from file name.

Next you'll want to save the file up to the cloud. As with `ParseObject`, you can call `SaveAsync` to save the file to Parse.
**Saving a `ParseFile`:**

```cs
```csharp
await file.SaveAsync();
```

Finally, after the save completes, you can assign a `ParseFile` into a `ParseObject` just like any other piece of data:
You *must* save the `ParseFile` to Parse *before* you can associate it with a `ParseObject`. The `SaveAsync()` method uploads the file data to the Parse Server.

**Associating a `ParseFile` with a `ParseObject`:**

```cs
```csharp
var jobApplication = new ParseObject("JobApplication");
jobApplication["applicantName"] = "Joe Smith";
jobApplication["applicantResumeFile"] = file;
jobApplication["applicantResumeFile"] = file; // Assign the saved ParseFile
await jobApplication.SaveAsync();
```

Retrieving it back involves downloading the resource at the `ParseFile`'s `Url`. Here we retrieve the resume file off another JobApplication object:

```cs
var applicantResumeFile = anotherApplication.Get<ParseFile>("applicantResumeFile");
string resumeText = await new HttpClient().GetStringAsync(applicantResumeFile.Url);
**Retrieving a `ParseFile`:**

You retrieve the `ParseFile` object itself from the `ParseObject`, and then you can access its URL to download the file data.

```csharp
// Assuming you have a jobApplication object:
ParseFile? applicantResumeFile = jobApplication.Get<ParseFile>("applicantResumeFile");

if (applicantResumeFile != null)
{
Download the file using HttpClient (more versatile)
using (HttpClient client = new HttpClient())
{
// As a byte array:
byte[] downloadedData = await client.GetByteArrayAsync(applicantResumeFile.Url);

// As a string (if it's text):
string resumeText = await client.GetStringAsync(applicantResumeFile.Url);

// To a Stream (for larger files, or to save directly to disk):
using (Stream fileStream = await client.GetStreamAsync(applicantResumeFile.Url))
{
// Process the stream (e.g., save to a file)
using (FileStream outputStream = File.Create("downloaded_resume.txt"))
{
await fileStream.CopyToAsync(outputStream);
}
}
}

}
```

## Progress
**Important Considerations for Retrieval:**

It's easy to get the progress of `ParseFile` uploads by passing a `Progress` object to `SaveAsync`. For example:
* **`Get<ParseFile>()`:** You retrieve the `ParseFile` *object* itself using `Get<ParseFile>()`. This object contains metadata like the URL, filename, and content type. It does *not* contain the file *data* itself.
* **`Url` Property:** The `ParseFile.Url` property provides the publicly accessible URL where the file data can be downloaded.
* **`HttpClient`:** The recommended way to download the file data is to use `HttpClient`. This gives you the most flexibility (handling different file types, large files, etc.).
* **`GetBytesAsync()`, `GetDataStreamAsync()`:** `ParseFile` provides convenient methods `GetBytesAsync()` and `GetDataStreamAsync()` to download data.
* **Stream Management:** Always wrap Stream and `HttpClient` in using statements, to release resources.

```cs
byte[] data = System.Text.Encoding.UTF8.GetBytes("Working at Parse is great!");
ParseFile file = new ParseFile("resume.txt", data);
## Progress Reporting (WIP..)

await file.SaveAsync(new Progress<ParseUploadProgressEventArgs>(e => {
// Check e.Progress to get the progress of the file upload
}));
```

You can delete files that are referenced by objects using the [REST API]({{ site.baseUrl }}/rest/guide/#deleting-files). You will need to provide the master key in order to be allowed to delete a file.

If your files are not referenced by any object in your app, it is not possible to delete them through the REST API. You may request a cleanup of unused files in your app's Settings page. Keep in mind that doing so may break functionality which depended on accessing unreferenced files through their URL property. Files that are currently associated with an object will not be affected.
## Deleting Files

*Files can be deleted from your server, freeing storage space.*

* **Referenced Files:** You can delete files that are *referenced* by objects using the REST API (requires the master key). *This SDK does not provide a direct method for deleting files via the client.* The documentation you provided links to the REST API documentation for this.

* **Unreferenced Files:** Files that are *not* referenced by any object in your app *cannot* be deleted via the REST API. You can request a cleanup of unused files in your app's Settings page (on the Parse Server dashboard). *Be careful*: Deleting unreferenced files might break functionality if your app relies on accessing them directly via their URL. Only files *currently* associated with an object are protected from this cleanup.

**Important Security Note:** File deletion via the REST API requires the *master key*. The master key should *never* be included in client-side code. File deletion should be handled by server-side logic (e.g., Cloud Code) or through the Parse Server dashboard.
10 changes: 6 additions & 4 deletions _includes/dotnet/geopoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Now that you have a bunch of objects with spatial coordinates, it would be nice

```cs
// User's location
var userGeoPoint = ParseUser.CurrentUser.Get<ParseGeoPoint>("location");
var user = await ParseClient.Instance.GetCurrentUser();
var userGeoPoint = user.Get<ParseGeoPoint>("location");

// Create a query for places
var query = ParseObject.GetQuery("PlaceObject");
var query = ParseClient.Instance.GetQuery("PlaceObject");
//Interested in locations near user.
query = query.WhereNear("location", userGeoPoint);
// Limit what could be a lot of points.
Expand All @@ -44,7 +46,7 @@ It's also possible to query for the set of objects that are contained within a p
```cs
var swOfSF = new ParseGeoPoint(37.708813, -122.526398);
var neOfSF = new ParseGeoPoint(37.822802, -122.373962);
var query = ParseObject.GetQuery("PizzaPlaceObject")
var query = ParseClient.Instance.GetQuery("PizzaPlaceObject")
.WhereWithinGeoBox("location", swOfSF, neOfSF);
var pizzaPlacesInSF = await query.FindAsync();
```
Expand All @@ -63,7 +65,7 @@ You can also query for `ParseObject`s within a radius using a `ParseGeoDistance`

```cs
ParseGeoPoint userGeoPoint = ParseUser.CurrentUser.Get<ParseGeoPoint>("location");
ParseQuery<ParseObject> query = ParseObject.GetQuery("PlaceObject")
ParseQuery<ParseObject> query = ParseClient.Instance.GetQuery("PlaceObject")
.WhereWithinDistance("location", userGeoPoint, ParseGeoDistance.FromMiles(5));
IEnumerable<ParseObject> nearbyLocations = await query.FindAsync();
// nearbyLocations contains PlaceObjects within 5 miles of the user's location
Expand Down
Loading