Learn how to use the .NET MAUI Community Toolkit CameraView control in your app, from initializing to creating the user interface.
A functionality that is common in mobile applications is the ability to work with the camera. Although the .NET MAUI framework includes classes and methods to use the camera, having a component that allows creating interfaces that offer additional functionality helps speed up application development. That is why in this article, we will examine the CameraView control of the .NET MAUI Community Toolkit.
What Is the CameraView Control
CameraView is a control within the CommunityToolkit.Maui.Camera package that helps perform tasks beyond taking photos and videos, allowing control of features such as flash, zoom, saving files and even hooks for reacting to different events.
This allows you to create experiences within your application without needing to navigate to an external application, giving you full control of the buttons, visual state and destination of captured files.
Using the CameraView Control in Your .NET MAUI Applications
Let’s demonstrate the CameraView control’s potential. To do this, in your project open the NuGet package manager and install the following packages:
CommunityToolkit.Maui.Camera
CommunityToolkit.Mvvm
The first package is the one we need to use the control and the APIs, while the second will allow using the MVVM pattern more easily in the project.
Once you have installed the camera control package, you will see a ReadMe.txt file with the steps to follow to correctly configure the control. This file tells us that we must go to MauiProgram.cs, adding the UseMauiCommunityToolkitCamera method for the purpose of initializing the control:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkitCamera()
...
}
}
With the control registered, we are ready to try it.
Declaring Permissions Per Platform
Although the readme doesn’t specify it, it is well known that to use hardware features on physical devices you must request permission from the user to use them. In our case, it is necessary to request permissions to use the camera and the microphone, which must be configured differently on each platform.
For Android, you should open the file Platforms/Android/AndroidManifest.xml, adding:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" />
...
<uses-permission android:name="android.permission.CAMERA" />
<!--Optional. Only for video recording-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
For iOS, you should open the file Platforms/iOS/Info.plist, adding the following privacy descriptions:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>NSCameraUsageDescription</key>
<string>PROVIDE YOUR REASON HERE</string>
<key>NSMicrophoneUsageDescription</key>
<string>PROVIDE YOUR REASON HERE</string>
</dict>
</plist>
For Windows no additional action is required.
Creating a ViewModel to Manage the Control
Once we’ve configured the project to request permissions from the user, let’s create the view model, which will allow managing the state of the control. For this, I created a class called MainViewModel with the following content.
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string statusMessage = "Grant permissions to start the camera.";
[ObservableProperty]
string cameraName = "No camera selected";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasPhoto))]
string? lastPhotoPath;
[ObservableProperty]
string? lastVideoPath;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(RecordButtonText))]
[NotifyPropertyChangedFor(nameof(RecordButtonColor))]
bool isRecording;
[ObservableProperty]
bool isTorchOn;
[ObservableProperty]
float currentZoom = 1.0f;
[ObservableProperty]
float minimumZoom = 1.0f;
[ObservableProperty]
float maximumZoom = 4.0f;
[ObservableProperty]
CameraFlashMode selectedFlashMode = CameraFlashMode.Off;
[ObservableProperty]
CameraInfo? selectedCamera;
public IReadOnlyList<CameraFlashMode> FlashModes { get; } = Enum.GetValues<CameraFlashMode>();
public bool HasPhoto => !string.IsNullOrWhiteSpace(LastPhotoPath);
public string RecordButtonText => IsRecording ? "Stop video" : "Record video";
public Color RecordButtonColor => IsRecording ? Colors.Firebrick : Colors.DarkGreen;
public void UpdateCameraInfo()
{
if (SelectedCamera is null)
{
CameraName = "No camera selected";
return;
}
CameraName = $"{SelectedCamera.Name} ({SelectedCamera.Position})";
MinimumZoom = Math.Max(1.0f, SelectedCamera.MinimumZoomFactor);
MaximumZoom = Math.Max(MinimumZoom, SelectedCamera.MaximumZoomFactor);
CurrentZoom = MinimumZoom;
}
partial void OnSelectedCameraChanged(CameraInfo? value)
{
UpdateCameraInfo();
}
}
In the previous code, I created some properties that can help us manage controls in the UI, for example:
CameraName: To indicate which camera is selectedLastPhotoPathandLastVideoPath: To show the latest paths for photos and videosIsRecording: To maintain the recording stateCurrentZoom,MinimumZoomandMaximumZoom: To control zoom valuesOnSelectedCameraChanged(...)andUpdateCameraInfo(): To update values when there is a camera change
With the base viewmodel ready, it’s time to create the graphical interface.
Creating the UI for the Application
It’s time to create the graphical interface page for the application. We’ll do this by creating a ContentPage, replacing the content with the following:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<Grid RowDefinitions="*,Auto" Padding="16" RowSpacing="12">
<Border Grid.Row="0" StrokeThickness="0" BackgroundColor="Black">
<Grid>
<toolkit:CameraView
x:Name="Camera"
CameraFlashMode="{Binding SelectedFlashMode}"
IsTorchOn="{Binding IsTorchOn}"
SelectedCamera="{Binding SelectedCamera}"
ZoomFactor="{Binding CurrentZoom}" />
<Border
Margin="12"
Padding="10,6"
BackgroundColor="#99000000"
StrokeThickness="0"
HorizontalOptions="Start"
VerticalOptions="Start">
<Label Text="{Binding CameraName}" TextColor="White" FontSize="12" />
</Border>
</Grid>
</Border>
<ScrollView Grid.Row="1" MaximumHeightRequest="360">
<VerticalStackLayout Spacing="14">
<Label Text="CameraView media demo" FontSize="22" FontAttributes="Bold" />
<Label Text="{Binding StatusMessage}" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="12">
<Button Text="Take photo" Clicked="OnTakePhotoClicked" />
<Button
Grid.Column="1"
Text="{Binding RecordButtonText}"
Clicked="OnRecordVideoClicked"
BackgroundColor="{Binding RecordButtonColor}" />
</Grid>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" RowDefinitions="Auto,Auto,Auto">
<Label Text="Zoom" VerticalOptions="Center" />
<Slider
Grid.Column="1"
Minimum="{Binding MinimumZoom}"
Maximum="{Binding MaximumZoom}"
Value="{Binding CurrentZoom}" />
<Label Grid.Row="1" Text="Torch" VerticalOptions="Center" />
<Switch Grid.Row="1" Grid.Column="1" IsToggled="{Binding IsTorchOn}" HorizontalOptions="Start" />
<Label Grid.Row="2" Text="Flash" VerticalOptions="Center" />
<Picker
Grid.Row="2"
Grid.Column="1"
ItemsSource="{Binding FlashModes}"
SelectedItem="{Binding SelectedFlashMode}" />
</Grid>
<Image
Source="{Binding LastPhotoPath}"
Aspect="AspectFill"
IsVisible="{Binding HasPhoto}" />
<Label Text="{Binding LastPhotoPath}" FontSize="12" LineBreakMode="TailTruncation" />
<Label Text="{Binding LastVideoPath}" FontSize="12" LineBreakMode="TailTruncation" />
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentPage>
In the previous code, we can highlight a few things:
- The namespace
xmlns:toolkitis used so that the control can be used viatoolkit:CameraView. - Properties such as
CameraFlashMode,IsTorchOn,SelectedCameraandZoomFactorare used, which will be connected to the viewmodel’s properties. - We use events linked to the code behind
OnTakePhotoClickedandOnRecordVideoClicked, because we work with capture APIs and streams.
Connecting the Content Page with the ViewModel
Once we have created the XAML code for the graphical interface, it’s time to connect it with the view model, as well as create the missing event handlers. To do this, replace the code behind of the XAML page with the following content:
public partial class MainPage : ContentPage
{
readonly MainViewModel viewModel = new();
Stream recordingStream = Stream.Null;
public MainPage()
{
InitializeComponent();
BindingContext = viewModel;
Camera.MediaCaptureFailed += OnMediaCaptureFailed;
}
protected override async void OnAppearing()
{
base.OnAppearing();
await RequestCameraPermissions();
await LoadCameras();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
Camera.MediaCaptureFailed -= OnMediaCaptureFailed;
recordingStream.Dispose();
}
private async Task RequestCameraPermissions()
{
var cameraStatus = await Permissions.RequestAsync<Permissions.Camera>();
if (cameraStatus is not PermissionStatus.Granted)
{
viewModel.StatusMessage = "You need to grant camera permission.";
return;
}
try
{
var microphoneStatus = await Permissions.RequestAsync<Permissions.Microphone>();
if (microphoneStatus is not PermissionStatus.Granted)
{
viewModel.StatusMessage = "You need to grant microphone permission to record video.";
}
}
catch (FileNotFoundException) when (OperatingSystem.IsWindows())
{
viewModel.StatusMessage = "On unpackaged Windows apps, microphone permission may require a packaged app.";
}
}
private async Task LoadCameras()
{
try
{
var cameras = await Camera.GetAvailableCameras(CancellationToken.None);
viewModel.SelectedCamera = cameras.FirstOrDefault();
viewModel.UpdateCameraInfo();
}
catch (Exception ex)
{
viewModel.StatusMessage = $"No cameras were found: {ex.Message}";
}
}
private void OnMediaCaptureFailed(object? sender, MediaCaptureFailedEventArgs e)
{
viewModel.StatusMessage = $"Capture error: {e.FailureReason}";
}
}
In the previous code you can notice some interesting things:
- As indicated by the CameraView documentation, permissions must be requested manually from the user to use the camera and microphone, in addition to the per-platform configuration. We do this when the page starts through the method
RequestCameraPermissions(). - It is possible to obtain the list of available cameras through
Camera.GetAvailableCameras. - In case an error occurs, we use the event
OnMediaCaptureFailedto handle it.
This is just the beginning. Now, let’s look at how to take a photo and a video.
Taking a Photo with the CameraView Control
In the code-behind, we need a method that allows taking a photograph when pressing a button. To achieve this, we will do it through the following code:
private async void OnTakePhotoClicked(object? sender, EventArgs e)
{
try
{
await using var photoStream = await Camera.CaptureImage(CancellationToken.None);
var photoPath = Path.Combine(FileSystem.AppDataDirectory, $"photo-{DateTime.Now:yyyyMMdd-HHmmss}.jpg");
await using var fileStream = File.Create(photoPath);
await photoStream.CopyToAsync(fileStream);
viewModel.LastPhotoPath = photoPath;
viewModel.StatusMessage = "Photo saved successfully.";
}
catch (Exception ex)
{
viewModel.StatusMessage = $"Unable to take photo: {ex.Message}";
}
}
In the previous method, we use Camera.CaptureImage to take a photograph, which returns a stream that we store locally. Next, we create a path using a safe location through FileSystemAppDataDirectory, which we use to save the stream with File.Create and CopyToAsync. Finally, we update the preview image and display the created path thanks to LastPhotoPath and StatusMessage.
Recording a Video with CameraView
To implement the recording process, we are going to create three methods. One controls the logic to know whether it is recording, starting or stopping the recording. The other two will allow starting and stopping the recording. The code looks as follows:
private async void OnRecordVideoClicked(object? sender, EventArgs e)
{
if (viewModel.IsRecording)
{
await StopRecording();
return;
}
await StartRecording();
}
private async Task StartRecording()
{
try
{
var videoPath = Path.Combine(FileSystem.AppDataDirectory, $"video-{DateTime.Now:yyyyMMdd-HHmmss}.mp4");
recordingStream = File.Create(videoPath);
await Camera.StartVideoRecording(recordingStream, CancellationToken.None);
viewModel.LastVideoPath = videoPath;
viewModel.IsRecording = true;
viewModel.StatusMessage = "Recording video...";
}
catch (Exception ex)
{
await recordingStream.DisposeAsync();
recordingStream = Stream.Null;
viewModel.StatusMessage = $"Unable to start video recording: {ex.Message}";
}
}
private async Task StopRecording()
{
try
{
await Camera.StopVideoRecording(CancellationToken.None);
await recordingStream.DisposeAsync();
recordingStream = Stream.Null;
viewModel.IsRecording = false;
viewModel.StatusMessage = "Video saved successfully.";
}
catch (Exception ex)
{
viewModel.StatusMessage = $"Unable to stop video recording: {ex.Message}";
}
}
In the previous code, the methods do the following:
OnRecordVideoClicked(...): Through the propertyIsRecording, it determines which method should be invoked when the user presses the button in the UI.StartVideoRecording(): It obtains a path using the current time to name the file and creates a stream at that location. Then, it uses the methodCamera.StartVideoRecordingto start the recording. It is important to keep the stream open while the recording is active.StopRecording: This ends the capture usingCamera.StopVideoRecording. After stopping the recording, the stream is closed to release the file.
Testing the Application
After adding all the previous code to the application, it’s time to test the functionality. When running the application, we get the following result:

You can see that all features work correctly, and that it’s possible to take photos and videos within the same application.
Conclusion
In this article you have learned about the CameraView control from the Community Toolkit, and how to integrate it into your own .NET MAUI applications. We went step by step to initialize it, configure permissions, create the view model and the user interface. With what you’ve learned, you can now create your own experiences using the control in more complex scenarios.


















Consult the detailed [Database Decision Matrix](./references/database_decision_matrix.md) for complete comparison tables.
---
### Step 2: Select the Target Skills
Map the workload to the primary database skill and check for relevant **Well-Architected Framework** pillars:
#### A. Database Engine Selection
* **Cloud SQL (`cloud-sql-basics` / `cloud-sql-mysql` / `cloud-sql-postgresql` / `cloud-sql-sqlserver`)**:
* *Select when:* Straightforward relational databases (MySQL, PostgreSQL, SQL Server), standard web apps, migration-friendly lift-and-shift via Database Migration Service (DMS).
* **AlloyDB for PostgreSQL (`alloydb-basics` / `alloydb`)**:
* *Select when:* Demanding enterprise PostgreSQL workloads, up to 4x transactional performance, up to 100x faster analytical/HTAP queries with columnar engine, or in-database vector search (Vertex AI / ScaNN integration).
* **Cloud Spanner (`spanner`)**:
* *Select when:* Mission-critical global databases, massive scale exceeding single-instance limits, synchronous multi-region replication, horizontal scaling, and 99.999% availability with zero downtime.
* **Discovery / Undecided (`cloud-databases-onboarding`)**:
* *Select when:* User is unsure or needs interactive discovery across relational, NoSQL (Firestore, Bigtable), and analytical stores.
#### B. Well-Architected Framework Triggers
Check if cross-cutting architectural pillars should be retrieved:
* **Reliability & HA:** Mentions of 99.999% SLA, DR, RTO/RPO, regional failover → Fetch `well-architected-reliability`.
* **Security & IAM:** Questions about Private IP, PSA, CMEK encryption, or IAM DB auth → Fetch `google-cloud-recipe-auth`.
* **Performance & Tuning:** Connection pooling, read replicas, latency optimization → Fetch `google-cloud-solution-architecture`.
* **Cost Optimization:** Committed Use Discounts (CUDs), rightsizing, auto-pause → Fetch `google-cloud-solution-architecture`.
> 